/** * 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