/**
* Issue Board 卡片:`/issue` 的飞书原生入口。
*
* 三个视图,同一张卡片就地切换(不新发消息,避免刷屏):
* 看板 board → 点「领取」→ 确认 confirm(选仓库)→ 点「确认领取」→ 结果 result
*
* 领取由**你 @ 的那个 bot** 承接:`larkAppId` 就是承接方,不需要再选。想让别的 bot 干,
* 就去 @ 它发 `/issue`。少一次选择,语义也天然。
*
* 仓库不新造配置:候选来自 `scanMultipleProjects(configuredWorkingDirs(bot))` —— 现有仓库
* 选择卡片用的同一套扫描。注意**不能**直接用 `workingDirs` 原值:实测发现它常常配的是一个
* 工作区父目录(`~/claude-code-workspace`)而不是仓库列表,直接拿来当候选,选中的会是工作区
* 根目录,等于让 agent 在错误的位置动手。扫描一层才拿得到真正的仓库和 worktree。
*
* 平台只存展示用的 `targetRepoLabel`,这里按仓库名做一次匹配把它预选上,匹配不上就让人在
* 下拉里挑——不拦人,也不猜。
*
* Security(沿用 groups-card 的约定):
* - `action.value` 不经 Lark 校验,**绝不**从里面读身份字段;调用者身份只认 operator.*
* - 权限门在命令入口和每次回调都要跑一遍
*
* 只吃纯数据,不反向依赖 store / platform client,避免循环依赖。
*/
import type { Locale } from '../../i18n/index.js';
export declare const ISSUE_ACTION_REFRESH: "issue_refresh";
export declare const ISSUE_ACTION_PAGE: "issue_page";
export declare const ISSUE_ACTION_TEAM: "issue_team";
export declare const ISSUE_ACTION_CLAIM_OPEN: "issue_claim_open";
export declare const ISSUE_ACTION_CLAIM_DIR: "issue_claim_dir";
export declare const ISSUE_ACTION_CLAIM_CONFIRM: "issue_claim_confirm";
export declare const ISSUE_ACTION_CLAIM_CANCEL: "issue_claim_cancel";
export interface IssueRowData {
issueId: string;
title: string;
repoLabel?: string;
/** CAS 基线,随卡片往返——领取时要拿它做 expectedStateRev。 */
stateRev: number;
/** 已被领取时展示领取人;本机领的还会有 chatId。 */
claimedByName?: string;
chatId?: string;
}
export interface IssueBoardCardData {
teamId: string;
teamName: string;
teams: Array<{
teamId: string;
teamName: string;
}>;
sections: {
needsAttention: IssueRowData[];
todo: IssueRowData[];
inProgress: IssueRowData[];
inReview: IssueRowData[];
done: IssueRowData[];
};
/** todo 段的页码(只有待领取需要翻页,其它段是概览计数)。 */
page: number;
/**
* 发起人的 `ou_*`。会被打进**每一个** action.value,回调时与 Lark 校验过的
* operator.open_id 比对——群里别人点了不算。
*
* 不是洁癖:平台的 claim 按**本机 owner** 记,不按点击者记。谁点都能领的话,任务记在
* owner 头上而实际点的是别人,归属直接错位。
*/
invokerOpenId: string;
}
/** 一个可选仓库。来自 project-scanner 的 `ProjectInfo`,只取卡片用得到的三个字段。 */
export interface RepoChoice {
name: string;
path: string;
branch?: string;
}
export interface ClaimConfirmCardData {
teamId: string;
issueId: string;
title: string;
repoLabel?: string;
stateRev: number;
/** 扫出来的候选仓库。空数组时不给确认按钮并说明原因。 */
repos: RepoChoice[];
/** 当前选中的仓库路径(首次进入由 matchRepo 预选)。 */
selectedDir?: string;
/** 见 IssueBoardCardData.invokerOpenId。 */
invokerOpenId: string;
}
export type ClaimResultCardData = {
ok: true;
title: string;
chatId: string;
chatName: string;
shareLink?: string;
} | {
ok: false;
title: string;
stage: string;
reason: string;
hint?: string;
};
/**
* 把平台的展示标签匹配到本机某个仓库,返回它的路径。
*
* 平台只有 `targetRepoLabel`("botmux"、"botmux 平台"…),是给人看的,没有路径。这里做的
* 是**预选**不是决定:匹配上就把下拉默认值设好省一次点击,匹配不上返回 undefined,让人自己
* 在下拉里挑。刻意不做模糊匹配——猜错仓库会让 agent 在错误的地方动手,代价远大于多点一下。
*
* 优先精确命中仓库名,再退一步允许去掉空格/下划线/连字符后相等("botmux 平台" 这种带空格
* 的标签能对上 `botmux-platform`)。前缀不算命中:`bot` 不该选中 `botmux`。
*/
export declare function matchRepo(label: string | undefined, repos: RepoChoice[]): string | undefined;
/**
* 按与标签的相关度给候选仓库排序并截断到 Lark 的选项上限。
*
* 与 {@link matchRepo} 分工明确:那个决定**选中谁**(严格,宁缺毋滥),这个只决定**先显示
* 谁**(宽松,含子串即可)。排序永远不会替人做选择,所以这里放宽是安全的——而不放宽的话,
* 标签没精确命中时人得在几十个仓库里自己翻,实测一个工作区能扫出 58 个。
*
* 截断是必要的:超过上限 Lark 会拒绝或静默丢弃。被截掉的部分由调用方在卡片上说明,
* 不能让人以为"没有就是不存在"。
*/
export declare function rankRepos(label: string | undefined, repos: RepoChoice[], limit?: number): {
options: RepoChoice[];
truncated: number;
};
/**
* lark_md 文本位消毒。issue 的标题/正文/仓库标签都是**人在平台上自由填的**,直接拼进
* lark_md 会被标签与强调语法击穿:一个 `` 或 `` 就能改掉整张卡的观感,
* 一个落单的反引号能让后面半张卡变成代码块。
*
* 转义顺序抄 [[brand-template]] 的 `safeText`(那是仓库里唯一做对的一份):**反斜杠必须最先**
* ——否则 `\*` 里的反斜杠自成偶数对,让紧跟的 `*` 重新变回有效强调。groups-card 等 5 处旧
* 拷贝都缺这一步,这里不跟着抄错。
*/
export declare function escapeLarkMd(s: string): string;
/**
* 按**显示宽度**截断(CJK 算两列),不是按字符数。
*
* 按 `length` 截会让中英文两种标题在卡片上宽窄差一倍:30 个汉字铺满两行,30 个字母才半行。
* 看板每行右边还挂着「领取」按钮,正文列本来就窄,超了就换行、按钮被挤到下一行,一排下来
* 参差不齐。
*/
export declare function truncateDisplay(s: string, maxWidth: number): string;
/** 看板视图。只有「待领取」列出可操作的行,其余段给计数——卡片要能一眼扫完。 */
export declare function buildIssueBoardCard(data: IssueBoardCardData, _locale?: Locale): string;
/** 领取确认视图:选仓库 + 确认/取消。就地替换看板,不新发卡片。 */
export declare function buildClaimConfirmCard(data: ClaimConfirmCardData, _locale?: Locale): string;
/** 结果视图。失败时**明说失败在哪一步**——不同阶段的补救方式完全不同(见 issue-claim-flow)。 */
export declare function buildClaimResultCard(data: ClaimResultCardData, _locale?: Locale): string;
export interface IssueKickoffCardData {
title: string;
body?: string;
workingDir: string;
issueId: string;
/** 平台任务详情深链。拿不到平台地址时省略,卡片只是少一个按钮。 */
issueUrl?: string;
}
/**
* 开工播报:领取建群后**发在任务群里**的第一条消息。
*
* 为什么必须有这么一张卡:kickoff prompt 是**内部投递**的(daemon 直接建会话,见
* [[issue-command-deps]] 的 ActivateSession),它不经过飞书,群里一个字都看不到。而新建的
* 群是普通群、默认 reply mode 不是 shared,`handleBotAdded` 的那条 seed 也不会发。两下一
* 叠,群从建好到 agent 吐出第一段输出之间**完全是空的**——人进群只看到一个空群,不知道
* agent 有没有开始干、在干什么、在哪个仓库干。实测反馈就是「找不到他已经开始工作了,
* 干完了才汇报一下」。
*
* 所以这张卡要把「agent 现在拿到的是什么」原样摊开:任务标题、正文、工作目录、平台任务
* 深链。它同时也是释放入口的说明书——不写在这里,人根本不知道 `/issue release` 存在。
*/
export declare function buildIssueKickoffCard(data: IssueKickoffCardData, _locale?: Locale): string;
export interface IssueDeliveryCardData {
title?: string;
issueId: string;
/** agent 执行 `botmux report` 时写的交付说明。 */
report: string;
issueUrl?: string;
/** 重复交付(平台上已经是待验收)时措辞不同,别让人以为交付了两次。 */
alreadyInReview?: boolean;
}
/**
* 交付播报:`botmux report` 成功后发回任务群。
*
* 之所以必须发:平台的 `/status` 接口**只收状态、不收正文**(claimId/claimEpoch/sourceSeq/
* status/expectedStateRev,没有 note 字段)。交付说明在平台上无处可放,验收的人只会看到状态
* 变成「待验收」,完全不知道交付了什么。在补上平台侧的备注字段之前,群里是这段文字唯一
* 能落地的地方——否则它就只是 `botmux report` 的一行 stdout,谁也看不见。
*/
export declare function buildIssueDeliveryCard(data: IssueDeliveryCardData, _locale?: Locale): string;
export interface IssueStatusCardData {
issueId: string;
bindState: string;
/** 平台侧现状。undefined = 没拉到(网络/已归档),卡上必须说成"拉不到"而不是"没有"。 */
platform?: {
title: string;
status: string;
attentionReason?: string;
claimAgent?: string;
claimLabel?: string;
};
/** 平台上这条 claim 是否还归本机。undefined = 无法判定。 */
claimMine?: boolean;
/** 发件箱里还没发出去的回写条数。 */
pendingWrites: number;
/** 已被判死、不再重投的回写条数(平台明确拒绝)。 */
failedWrites?: number;
/** 最后一条判死行的错误原文。 */
lastFailure?: string;
/** 本机认为已同步到的状态(用于和平台对照)。 */
lastSyncedStatus?: string;
issueUrl?: string;
}
/**
* `/issue status`:本机与平台两边的现状对照。
*
* 卡片的重点不是"报个状态",而是把**两边不一致**摆出来——回写卡在发件箱、claim 已经被平台
* 收走、任务掉进需要关注,这三种情况在出问题时都是静默的,人只有主动查才看得见。所以:
* - claim 不归本机 → 顶格红字警告(这个群继续干下去的产出没人收)
* - 有待发回写 → 明说有几条、后台会重投,免得人以为状态更新丢了
* - 平台拉不到 → 说"拉不到",绝不说"任务不存在"(`findIssueById` 的 null 本就无法区分二者)
*/
export declare function buildIssueStatusCard(data: IssueStatusCardData, _locale?: Locale): string;
/** 失败阶段 → 给人的下一步提示。措辞对应 issue-claim-flow 里那张「失败留下什么」表。 */
export declare function claimFailureHint(stage: string): string | undefined;
//# sourceMappingURL=issue-card.d.ts.map