opencode-feishu-plugin 0.2.13 → 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -20,8 +20,9 @@ Connect [OpenCode](https://opencode.ai) to Feishu/Lark: **one Feishu topic = one
20
20
  | 🚀 **One-shot session** | `/new` opens a single form (directory + model + permission preset); submit to create a session and auto-open a topic |
21
21
  | ✅ **Card approvals** | Permission requests become cards: allow once / always / this session only / deny — signed tokens prevent forgery and replay |
22
22
  | 📊 **Real-time visible** | "Thinking" receipt → live tool-call cards → streaming text updates; footer shows the current model |
23
- | ⏹ **Controllable** | Every reply card has a "force stop" button; a watchdog auto-interrupts stuck sessions; native queue with `/steer` `/now` to cut in |
23
+ | ⏹ **Controllable** | Every reply card has a "force stop" button; a watchdog auto-interrupts stuck sessions; new messages **cut in by default** when busy (configurable to queue), `/steer` `/now` always available |
24
24
  | 📎 **Images / files** | Images and files sent in Feishu are downloaded and attached to the session, so vision/file-capable models can see and read them |
25
+ | 🧭 **One-sentence session** | Send a task in the main chat — the AI judges intent, finds the right working directory, and creates + starts the session in one tap |
25
26
  | 🖱 **Main-window menu** | Two fixed quick buttons above the chat input — "New session" and "Session list" (bot custom menu) — one tap to fire them |
26
27
  | 🚫 **No ports** | Full long-connection; no inbound port needed on the server |
27
28
 
@@ -127,6 +128,23 @@ After configuring the menu (setup step 6), two quick buttons stay pinned **above
127
128
  - After changing the menu, **re-publish the app version** (per the console: takes effect within ~5 minutes after publishing);
128
129
  - Message the bot at least once before using the menu (the plugin remembers the p2p chat from messages; any normal usage satisfies this).
129
130
 
131
+ ### AI session management (main chat)
132
+
133
+ Send plain text in the main chat and the AI judges the intent and handles it:
134
+
135
+ - **Create a session**: it parses directory / title / permission tier / model and turns the card in place into an **AI-prefilled create-session form** — confirm (or tweak) and tap "✅ 创建会话", and a topic opens with work started:
136
+
137
+ ```
138
+ You: fix the zlib download bug, use high-risk approval
139
+ → 📝 Create-session form (AI prefilled: dir `/Users/code/zlib`, permission "High-risk approval")
140
+ [✅ 创建会话] ← confirm to create + auto-open a topic
141
+ ```
142
+
143
+ - **List sessions**: say "what sessions do I have?" → a session list card appears directly (same as `/sessions`; paginate / enter / create);
144
+ - **Chit-chat / other**: the usual console hint card.
145
+
146
+ Guardrails: the directory must match a candidate (recent dirs + local session dirs) and pass allowedRoots validation; the model must be one from the available list; anything the AI cannot resolve is **left blank for you to fill in the form** — creation always goes through form confirmation. `quickNew: false` disables the whole thing.
147
+
130
148
  ### Inside a topic (work)
131
149
 
132
150
  A topic = a session; sending plain text is giving the AI a command.
@@ -174,6 +192,8 @@ When the agent asks via `question` or other form tools, the form becomes a Feish
174
192
  | `logFile` | string \| boolean | — | `true` = write `<configDir>/plugins/feishu.log`; recommended in server mode |
175
193
  | `approvalTtlMs` | number | `600000` | Approval token / card validity |
176
194
  | `staleExecutionMs` | number | `300000` | Watchdog threshold (0–60 min; **0 = disabled**; sessions waiting on a form / pending approval are never killed) |
195
+ | `quickNew` | boolean | `true` | Main-chat "one-sentence session": AI judges intent + finds the directory, proposal card creates in one tap; `false` = console-only mode |
196
+ | `busyDelivery` | `steer`\|`queue` | `steer` | Delivery for new messages while busy: `steer` = cut in and interrupt the current step; `queue` = native queueing (gentler for long commands) |
177
197
  | `gatewayLocation` | string | — | Only start the gateway at this location (or its subdirectories); empty = any location |
178
198
 
179
199
  Full config (including `cardMaxTables`, `topicStatus*`, `resumeSummary*`, `keepalive*`, `gatewayMatchGraceMs`) → [docs/advanced.en.md](./docs/advanced.en.md#full-configuration).
@@ -207,7 +227,7 @@ Full config (including `cardMaxTables`, `topicStatus*`, `resumeSummary*`, `keepa
207
227
  Iterating from real usage feedback; current plan:
208
228
 
209
229
  - [x] **Accept images / files**: done — downloaded automatically into the session working directory at `.opencode/temp/opencode-feishu-plugin/` (with a built-in `.gitignore`, so `git status` stays clean; override with `attachmentsDir`; default max 20MB per attachment).
210
- - [ ] **New messages cut in by default when busy**: currently new messages queue natively while a session is busy (manual cut-in via `/steer`, `/now`). Planned: new messages default to **cutting in immediately**, interrupting the current step to run first.
230
+ - [x] **New messages cut in by default when busy**: done — while busy, new messages **cut in immediately** (interrupting the current step to run first); set `busyDelivery: "queue"` to go back to native queueing (gentler for long-running commands).
211
231
 
212
232
  ## Advanced topics & development
213
233
 
package/README.md CHANGED
@@ -20,8 +20,9 @@
20
20
  | 🚀 **一键建会话** | `/new` 一张表单(目录 + 模型 + 权限档位)一次填完,提交即建会话并自动开话题 |
21
21
  | ✅ **卡片审批** | 权限请求变飞书卡片:允许一次 / 始终允许 / 本会话内允许 / 拒绝,自签 token 防伪防重放 |
22
22
  | 📊 **实时可见** | 「思考中」回执 → 工具调用实时上卡 → 文本流式更新,页脚显示当前模型 |
23
- | ⏹ **可控可停** | 每张回复卡带「强制停止」;看门狗自动中断卡死会话;忙时原生排队,`/steer` `/now` 可插队 |
23
+ | ⏹ **可控可停** | 每张回复卡带「强制停止」;看门狗自动中断卡死会话;忙时新消息**默认插队**(可配置为排队),`/steer` `/now` 随时可用 |
24
24
  | 📎 **图片 / 文件** | 飞书里的图片 / 文件自动下载并挂进会话,支持视觉 / 文件的模型直接看图、读文件 |
25
+ | 🧭 **一句话建会话** | 主聊天流发任务文本,AI 自动判断意图、找好工作目录,一键建会话并开始处理 |
25
26
  | 🖱 **主窗口菜单** | 机器人输入框上固定「新建会话」「会话列表」快捷按钮(机器人自定义菜单),一点即用 |
26
27
  | 🚫 **无端口** | 全程长连接,服务器无需开放任何入站端口 |
27
28
 
@@ -127,6 +128,23 @@ tail -f ~/.config/opencode/plugins/feishu.log # 应看到「飞书长连接已
127
128
  - 菜单配置修改后需**重新发布版本**(官方说明:发版成功后约 5 分钟内生效);
128
129
  - 首次使用菜单前,先给机器人发过至少一条消息(插件据消息记住单聊会话;正常使用必然满足)。
129
130
 
131
+ ### AI 会话管理(主聊天流)
132
+
133
+ 主聊天流发普通文本,AI 会判断意图并直接处理:
134
+
135
+ - **建会话**:解析 目录 / 标题 / 权限档位 / 模型,把卡片就地变成**AI 预填的建会话表单**——确认或微调后点「✅ 创建会话」即可,自动开话题开工:
136
+
137
+ ```
138
+ 你:帮我修一下 zlib 的下载 bug,用高风险审批
139
+ → 📝 建会话表单(AI 已预填:目录 `/Users/code/zlib`、权限「高风险审批」)
140
+ [✅ 创建会话] ← 确认后建会话 + 自动开话题
141
+ ```
142
+
143
+ - **列会话**:说「我有哪些会话」→ 直接出会话列表卡(等同 `/sessions`,可翻页 / 进话题 / 新建);
144
+ - **闲聊 / 其它**:照旧回管理台提示卡。
145
+
146
+ 防误伤:目录必须命中候选(最近使用 + 本机会话目录)且通过 allowedRoots 复核、模型必须命中可选列表;AI 解析不了的字段**留空由你在表单里补**,最终一定要经过表单确认。`quickNew: false` 可整体关闭。
147
+
130
148
  ### 话题内(干活)
131
149
 
132
150
  一个话题 = 一个会话,发普通文本就是给 AI 下指令。
@@ -174,6 +192,8 @@ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发
174
192
  | `logFile` | string \| boolean | — | `true` = 写 `<configDir>/plugins/feishu.log`,服务模式建议开启 |
175
193
  | `approvalTtlMs` | number | `600000` | 审批 token / 卡片有效期 |
176
194
  | `staleExecutionMs` | number | `300000` | 看门狗阈值(0–60 分钟;**0 = 关闭看门狗**;待答表单 / 未决审批期间不判卡死) |
195
+ | `quickNew` | boolean | `true` | 主聊天流「一句话建会话」:AI 判断意图 + 找目录,建议卡一键创建;`false` = 回到纯管理台模式 |
196
+ | `busyDelivery` | `steer`\|`queue` | `steer` | 忙时新消息投递方式:`steer` = 立即插队打断当前步骤;`queue` = 原生排队(长命令场景更温和) |
177
197
  | `gatewayLocation` | string | — | 只在该 location(及其子目录)启动网关;留空 = 任意 location 生效 |
178
198
 
179
199
  完整配置(含 `cardMaxTables`、`topicStatus*`、`resumeSummary*`、`keepalive*`、`gatewayMatchGraceMs` 等进阶项)见 [docs/advanced.md](./docs/advanced.md#完整配置项)。
@@ -207,7 +227,7 @@ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发
207
227
  按真实使用反馈迭代,当前规划:
208
228
 
209
229
  - [x] **接收图片 / 文件**:已支持——自动下载到**会话工作目录**下的 `.opencode/temp/opencode-feishu-plugin/`(内置 `.gitignore`,不污染 `git status`;可用 `attachmentsDir` 覆盖;单附件默认 ≤20MB)。
210
- - [ ] **忙时新消息默认插队**:目前会话忙时新消息默认排队(可用 `/steer`、`/now` 手动插队)。规划:忙时你发的新消息**默认直接插队**,立即打断当前步骤优先执行。
230
+ - [x] **忙时新消息默认插队**:已支持——忙碌时新消息**默认直接插队**(打断当前步骤优先执行);`busyDelivery: "queue"` 可切回原生排队(长命令场景更温和)。
211
231
 
212
232
  ## 高级主题与开发
213
233
 
package/dist/index.js CHANGED
@@ -144037,6 +144037,8 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
144037
144037
  );
144038
144038
  const attachmentTimeoutMs = clamp4(asNumber(merged.attachmentTimeoutMs, 3e4), 5e3, 12e4);
144039
144039
  const attachmentsDirRaw = asString(merged.attachmentsDir).trim();
144040
+ const quickNew = asBoolean(merged.quickNew, true);
144041
+ const busyDelivery = merged.busyDelivery === "queue" ? "queue" : "steer";
144040
144042
  const keepaliveIntervalMs = clamp4(
144041
144043
  asNumber(merged.keepaliveIntervalMs, 20 * 60 * 1e3),
144042
144044
  5 * 60 * 1e3,
@@ -144101,7 +144103,9 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
144101
144103
  acceptAttachments,
144102
144104
  attachmentMaxBytes,
144103
144105
  attachmentTimeoutMs,
144104
- ...attachmentsDirRaw ? { attachmentsDir: attachmentsDirRaw } : {}
144106
+ ...attachmentsDirRaw ? { attachmentsDir: attachmentsDirRaw } : {},
144107
+ quickNew,
144108
+ busyDelivery
144105
144109
  };
144106
144110
  }
144107
144111
  function loadConfigFile(env, deps, warnings) {
@@ -145930,8 +145934,8 @@ function readTimestamp(value3) {
145930
145934
  }
145931
145935
 
145932
145936
  // src/feishu/delivery.ts
145933
- function decideDelivery(running) {
145934
- return running ? "queue" : "steer";
145937
+ function decideDelivery(running, busyDelivery = "queue") {
145938
+ return running ? busyDelivery : "steer";
145935
145939
  }
145936
145940
  var ExecutionTracker = class {
145937
145941
  /** sessionID → 最近一次活动时间戳(ms)。 */
@@ -146078,6 +146082,14 @@ function withAssistantID(state, assistantMessageID) {
146078
146082
  if (!assistantMessageID || assistantMessageID === state.assistantMessageID) return state;
146079
146083
  return { ...state, assistantMessageID };
146080
146084
  }
146085
+ function lastPrefixTextIndex(blocks, text) {
146086
+ for (let i = blocks.length - 1; i >= 0; i -= 1) {
146087
+ const block = blocks[i];
146088
+ if (block.kind !== "text") continue;
146089
+ if (block.content && text.startsWith(block.content)) return i;
146090
+ }
146091
+ return -1;
146092
+ }
146081
146093
  function patchTool(state, id2, update2) {
146082
146094
  let found = false;
146083
146095
  const blocks = state.blocks.map((b) => {
@@ -146118,21 +146130,62 @@ function reduce6(state, event) {
146118
146130
  if (!event.delta) return state;
146119
146131
  const base2 = stepTransition(state, event.assistantMessageID);
146120
146132
  const last = base2[base2.length - 1];
146121
- const blocks = last && last.kind === "text" && last.streaming ? [...base2.slice(0, -1), { ...last, content: last.content + event.delta }] : [...base2, { kind: "text", content: event.delta, streaming: true }];
146133
+ const blocks = last && last.kind === "text" && last.streaming ? [...base2.slice(0, -1), { ...last, content: last.content + event.delta, ...event.assistantMessageID ? { msg: event.assistantMessageID } : {} }] : [...base2, { kind: "text", content: event.delta, streaming: true, ...event.assistantMessageID ? { msg: event.assistantMessageID } : {} }];
146122
146134
  const next = withAssistantID(state, event.assistantMessageID);
146123
146135
  return { ...next, blocks, footer: "streaming", terminal: "running" };
146124
146136
  }
146125
146137
  case "text.ended": {
146126
146138
  const blocks = state.blocks;
146127
- const last = blocks[blocks.length - 1];
146128
- let nextBlocks;
146129
- if (last && last.kind === "text" && last.streaming) {
146130
- nextBlocks = [...blocks.slice(0, -1), { ...last, content: event.text ?? last.content, streaming: false }];
146131
- } else if (event.text) {
146132
- nextBlocks = [...closeStreamingText(blocks), { kind: "text", content: event.text, streaming: false }];
146133
- } else {
146134
- nextBlocks = closeStreamingText(blocks);
146139
+ const endedText = event.text;
146140
+ const endedMsg = event.assistantMessageID;
146141
+ if (!endedMsg) {
146142
+ const last = blocks[blocks.length - 1];
146143
+ let nextBlocks2;
146144
+ if (last && last.kind === "text" && last.streaming) {
146145
+ nextBlocks2 = [...blocks.slice(0, -1), { ...last, content: endedText ?? last.content, streaming: false }];
146146
+ } else if (endedText) {
146147
+ const idx = lastPrefixTextIndex(blocks, endedText);
146148
+ nextBlocks2 = idx >= 0 ? blocks.map((b, i) => i === idx && b.kind === "text" ? { ...b, content: endedText, streaming: false } : b) : [...closeStreamingText(blocks), { kind: "text", content: endedText, streaming: false }];
146149
+ } else {
146150
+ nextBlocks2 = closeStreamingText(blocks);
146151
+ }
146152
+ return { ...state, blocks: nextBlocks2, footer: "streaming" };
146153
+ }
146154
+ let targetIdx = -1;
146155
+ for (let i = blocks.length - 1; i >= 0; i -= 1) {
146156
+ const b = blocks[i];
146157
+ if (b.kind === "text" && b.msg === endedMsg) {
146158
+ targetIdx = i;
146159
+ break;
146160
+ }
146161
+ }
146162
+ const tail = blocks[blocks.length - 1];
146163
+ if (targetIdx < 0 && tail && tail.kind === "text" && tail.streaming) targetIdx = blocks.length - 1;
146164
+ if (targetIdx < 0) {
146165
+ if (!endedText) return { ...state, blocks: closeStreamingText(blocks), footer: "streaming" };
146166
+ return {
146167
+ ...state,
146168
+ blocks: [
146169
+ ...closeStreamingText(blocks),
146170
+ { kind: "text", content: endedText, streaming: false, msg: endedMsg }
146171
+ ],
146172
+ footer: "streaming"
146173
+ };
146135
146174
  }
146175
+ const target = blocks[targetIdx];
146176
+ let earlierLen = 0;
146177
+ for (let i = 0; i < targetIdx; i += 1) {
146178
+ const b = blocks[i];
146179
+ if (b.kind === "text" && b.msg === endedMsg) earlierLen += b.content.length;
146180
+ }
146181
+ let content = target.kind === "text" ? target.content : "";
146182
+ if (endedText !== void 0 && endedText.length >= earlierLen) {
146183
+ const suffix = endedText.slice(earlierLen);
146184
+ if (suffix.length >= content.length) content = suffix;
146185
+ }
146186
+ const nextBlocks = blocks.map(
146187
+ (b, i) => i === targetIdx && b.kind === "text" ? { ...b, content, streaming: false } : b
146188
+ );
146136
146189
  return { ...state, blocks: nextBlocks, footer: "streaming" };
146137
146190
  }
146138
146191
  case "tool.input.started": {
@@ -146830,6 +146883,15 @@ function createRunController(deps) {
146830
146883
  }
146831
146884
  if (!card) return;
146832
146885
  update2(card, event, false);
146886
+ if (isActivityEvent(event.type)) {
146887
+ deps.log.debug("\u8FD0\u884C\u5361\u5757\u7ED3\u6784", {
146888
+ sessionID,
146889
+ event: event.type,
146890
+ blocks: card.state.blocks.map(
146891
+ (b) => b.kind === "text" ? `T${b.streaming ? "*" : ""}${b.content.length}` : `tool:${b.tool.name}`
146892
+ )
146893
+ });
146894
+ }
146833
146895
  },
146834
146896
  hasActive(sessionID) {
146835
146897
  return sessions.get(sessionID)?.active !== void 0;
@@ -147895,6 +147957,7 @@ function decideRoute(facts) {
147895
147957
  }
147896
147958
 
147897
147959
  // src/feishu/session-list.ts
147960
+ var INTERNAL_SESSION_TITLE_PREFIX = "\u2699\uFE0F \u5185\u90E8\u751F\u6210\uFF08\u4E34\u65F6";
147898
147961
  function normalizeSessionList(raw) {
147899
147962
  const array3 = extractArray(raw);
147900
147963
  if (!array3) return void 0;
@@ -147903,6 +147966,7 @@ function normalizeSessionList(raw) {
147903
147966
  for (const item of array3) {
147904
147967
  const entry = normalizeSessionInfo(item);
147905
147968
  if (!entry || seen.has(entry.sessionID)) continue;
147969
+ if (entry.title.startsWith(INTERNAL_SESSION_TITLE_PREFIX)) continue;
147906
147970
  seen.add(entry.sessionID);
147907
147971
  out.push(entry);
147908
147972
  }
@@ -148502,6 +148566,99 @@ function parsePage(value3) {
148502
148566
  const parsed = typeof value3 === "number" ? value3 : typeof value3 === "string" ? Number.parseInt(value3, 10) : Number.NaN;
148503
148567
  return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
148504
148568
  }
148569
+
148570
+ // src/feishu/quick-new-cards.ts
148571
+ function buildQuickNewThinkingCard() {
148572
+ return {
148573
+ schema: "2.0",
148574
+ config: { update_multi: true },
148575
+ header: { title: { tag: "plain_text", content: "\u{1F914} \u6B63\u5728\u8BC6\u522B\u610F\u56FE\u2026" }, template: "blue" },
148576
+ body: {
148577
+ elements: [
148578
+ {
148579
+ tag: "markdown",
148580
+ content: "\u6B63\u5728\u5224\u65AD\u8FD9\u6761\u6D88\u606F\u7684\u610F\u56FE\uFF0C\u5E76\u89E3\u6790\u76EE\u5F55 / \u6743\u9650 / \u6A21\u578B\u7B49\u4FE1\u606F\u2026"
148581
+ }
148582
+ ]
148583
+ }
148584
+ };
148585
+ }
148586
+
148587
+ // src/session/quick-new.ts
148588
+ var QUICK_NEW_PERMS = ["readonly", "edit", "askHigh", "trust"];
148589
+ var QUICK_NEW_INSTRUCTION = [
148590
+ "\u4F60\u662F\u98DE\u4E66 AI \u52A9\u624B\u300C\u7BA1\u7406\u53F0\u300D\u7684\u610F\u56FE\u8BC6\u522B\u5668\u3002\u7528\u6237\u5728\u7BA1\u7406\u53F0\uFF08\u8FD8\u6CA1\u6709\u4F1A\u8BDD\uFF09\u53D1\u6765\u4E00\u6761\u6D88\u606F\u3002",
148591
+ "\u5224\u65AD\u610F\u56FE\u5E76\u5C3D\u91CF\u89E3\u6790\u5EFA\u4F1A\u8BDD\u5B57\u6BB5\u3002\u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\uFF1A",
148592
+ '{"intent":"create|list|chat","dir":"<\u7EDD\u5BF9\u8DEF\u5F84\u6216\u7A7A\u5B57\u7B26\u4E32>","title":"<\u4E0D\u8D85\u8FC720\u5B57\u7684\u4F1A\u8BDD\u6807\u9898>","perm":"readonly|edit|askHigh|trust|\u7A7A","model":"<providerID/modelID \u6216\u7A7A>","reason":"<\u4E00\u53E5\u8BDD\u7406\u7531>"}',
148593
+ "\u89C4\u5219\uFF1A",
148594
+ "- \u5217\u51FA/\u67E5\u770B\u4F1A\u8BDD \u2192 intent=list\uFF0C\u5176\u4F59\u5B57\u6BB5\u7559\u7A7A\u3002",
148595
+ "- \u9700\u8981\u65B0\u5EFA\u4F1A\u8BDD\u6267\u884C\u7684\u5F00\u53D1/\u64CD\u4F5C\u4EFB\u52A1 \u2192 intent=create\uFF1B\u95F2\u804A\u3001\u95EE\u5019\u3001\u8BE2\u95EE\u7528\u6CD5 \u2192 chat\u3002",
148596
+ "- dir \u53EA\u80FD\u4ECE\u5019\u9009\u76EE\u5F55\u4E2D\u9009\u62E9\uFF08\u8BED\u4E49\u6700\u5339\u914D\u7684\u90A3\u4E2A\uFF09\uFF1B\u627E\u4E0D\u5230\u5408\u9002\u7684\u5C31\u7559\u7A7A\u5B57\u7B26\u4E32\u3002",
148597
+ "- perm \u4F9D\u636E\u7528\u6237\u8868\u8FF0\uFF08\u53EA\u8BFB\u2192readonly\u3001\u53EF\u7F16\u8F91\u2192edit\u3001\u9AD8\u98CE\u9669\u2192askHigh\u3001\u5B8C\u5168\u4FE1\u4EFB\u2192trust\uFF09\uFF1B\u7528\u6237\u6CA1\u8BF4\u5C31\u7559\u7A7A\u3002",
148598
+ "- model \u53EA\u80FD\u4ECE\u5019\u9009\u6A21\u578B\u4E2D\u7CBE\u786E\u590D\u5236 providerID/modelID\uFF1B\u7528\u6237\u6CA1\u8BF4\u5C31\u7559\u7A7A\u3002"
148599
+ ].join("\n");
148600
+ function buildQuickNewPrompt(input) {
148601
+ const dirs = input.candidates.slice(0, 40).map((c) => `- ${c.path}${c.label ? `\uFF08${c.label}\uFF09` : ""}`).join("\n");
148602
+ const models = (input.models ?? []).slice(0, 30).map((m) => `- ${m.providerID}/${m.id}${m.name ? `\uFF08${m.name}\uFF09` : ""}`).join("\n");
148603
+ return [
148604
+ QUICK_NEW_INSTRUCTION,
148605
+ "",
148606
+ "\u5019\u9009\u76EE\u5F55\uFF1A",
148607
+ dirs || "\uFF08\u65E0\uFF09",
148608
+ "",
148609
+ "\u5019\u9009\u6A21\u578B\uFF1A",
148610
+ models || "\uFF08\u65E0\uFF09",
148611
+ "",
148612
+ "\u7528\u6237\u6D88\u606F\uFF1A",
148613
+ input.text.slice(0, 2e3)
148614
+ ].join("\n");
148615
+ }
148616
+ function isPerm(value3) {
148617
+ return typeof value3 === "string" && QUICK_NEW_PERMS.includes(value3);
148618
+ }
148619
+ function parseQuickNewDecision(raw) {
148620
+ if (!raw) return void 0;
148621
+ const text = raw.trim();
148622
+ const start2 = text.indexOf("{");
148623
+ const end = text.lastIndexOf("}");
148624
+ if (start2 < 0 || end <= start2) return void 0;
148625
+ let parsed;
148626
+ try {
148627
+ parsed = JSON.parse(text.slice(start2, end + 1));
148628
+ } catch {
148629
+ return void 0;
148630
+ }
148631
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
148632
+ const obj = parsed;
148633
+ const intent = obj.intent === "create" || obj.intent === "list" || obj.intent === "chat" ? obj.intent : void 0;
148634
+ if (!intent) return void 0;
148635
+ const dir = typeof obj.dir === "string" ? obj.dir.trim() : typeof obj.directory === "string" ? obj.directory.trim() : "";
148636
+ const title = typeof obj.title === "string" ? obj.title.trim().slice(0, 30) : "";
148637
+ const perm = isPerm(obj.perm) ? obj.perm : void 0;
148638
+ const model = typeof obj.model === "string" ? obj.model.trim() : "";
148639
+ const reason = typeof obj.reason === "string" ? obj.reason.trim().slice(0, 200) : "";
148640
+ return {
148641
+ intent,
148642
+ ...dir ? { directory: dir } : {},
148643
+ ...title ? { title } : {},
148644
+ ...perm ? { perm } : {},
148645
+ ...model ? { model } : {},
148646
+ ...reason ? { reason } : {}
148647
+ };
148648
+ }
148649
+ function matchCandidateDirectory(directory, candidates) {
148650
+ if (!directory) return void 0;
148651
+ const normalize2 = (p) => p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
148652
+ const target = normalize2(directory);
148653
+ const hit = candidates.find((c) => normalize2(c.path) === target);
148654
+ return hit?.path;
148655
+ }
148656
+ function matchModelOption(model, models) {
148657
+ if (!model) return void 0;
148658
+ const norm = (s) => s.trim().toLowerCase();
148659
+ const target = norm(model);
148660
+ return models.find((m) => norm(`${m.providerID}/${m.id}`) === target) ?? models.find((m) => norm(m.id) === target) ?? models.find((m) => m.name !== void 0 && norm(m.name) === target);
148661
+ }
148505
148662
  var SYSTEM_DIRS = [
148506
148663
  "/etc",
148507
148664
  "/usr",
@@ -149856,6 +150013,19 @@ async function openSetupForm(ctx, message, title) {
149856
150013
  const res = await ctx.deps.sender.sendCard(message.chatId, card);
149857
150014
  if (!res.ok) ctx.deps.log.warn("\u8868\u5355\u5361\u53D1\u9001\u5931\u8D25", { chatId: message.chatId, error: res.error ?? "unknown" });
149858
150015
  }
150016
+ async function buildPrefilledSetupForm(ctx, chatId, anchorMessageId, prefill) {
150017
+ let state = await ctx.deps.wizard.get(chatId);
150018
+ if (!state) state = await ctx.deps.wizard.start(chatId, prefill.title, anchorMessageId);
150019
+ const next = {
150020
+ ...state,
150021
+ ...prefill.title !== void 0 ? { title: prefill.title } : {},
150022
+ ...prefill.dir !== void 0 ? { dir: prefill.dir } : {},
150023
+ ...prefill.model !== void 0 ? { model: prefill.model } : {},
150024
+ ...prefill.perm !== void 0 ? { perm: prefill.perm } : {}
150025
+ };
150026
+ await ctx.deps.wizard.set(chatId, next);
150027
+ return renderFormCard(ctx, next);
150028
+ }
149859
150029
  async function cmdDir(ctx, message, args2) {
149860
150030
  if (message.threadId) {
149861
150031
  await ctx.reply(message, threadForbiddenText("dir"));
@@ -150250,7 +150420,8 @@ function createSetupWizardApi(ctx) {
150250
150420
  cmdCancel: (message) => cmdCancel(ctx, message),
150251
150421
  applySetupCardAction: (action, value3) => applySetupCardAction(ctx, action, value3),
150252
150422
  applySetupFormSubmit: (action) => applySetupFormSubmit(ctx, action),
150253
- sendSetupFormForChat: (chatId, anchorMessageId, openId) => sendSetupFormForChat(ctx, chatId, anchorMessageId, openId)
150423
+ sendSetupFormForChat: (chatId, anchorMessageId, openId) => sendSetupFormForChat(ctx, chatId, anchorMessageId, openId),
150424
+ buildPrefilledSetupForm: (chatId, anchorMessageId, prefill) => buildPrefilledSetupForm(ctx, chatId, anchorMessageId, prefill)
150254
150425
  };
150255
150426
  }
150256
150427
 
@@ -150457,10 +150628,17 @@ async function patchResumeSummary(ctx, sessionID, dir, messageId, cardInput, per
150457
150628
  ...dir ? { directory: dir } : {},
150458
150629
  timeoutMs: ctx.deps.resumeSummaryTimeoutMs ?? 15e3
150459
150630
  });
150631
+ const base2 = await ctx.deps.sessionMap.getRootCard(sessionID);
150632
+ if (base2?.compactPending || base2?.summaryLabel === "\u5DF2\u538B\u7F29 \xB7 \u4F1A\u8BDD\u6458\u8981") {
150633
+ if (base2.summaryPending) {
150634
+ await ctx.deps.sessionMap.setRootCard(sessionID, { ...base2, summaryPending: false });
150635
+ }
150636
+ ctx.deps.log.info("\u6062\u590D\u5361\u6458\u8981\u8BA9\u4F4D\u538B\u7F29\u6D41\u7A0B\uFF08\u8DF3\u8FC7 patch\uFF09", { sessionID, source: outcome.source });
150637
+ return;
150638
+ }
150460
150639
  const summary = outcome.summary ?? "\uFF08\u6458\u8981\u751F\u6210\u5931\u8D25\uFF0C\u53EF\u76F4\u63A5\u53D1\u6D88\u606F\u7EE7\u7EED\uFF09";
150461
150640
  const summaryLabel = SUMMARY_LABEL[outcome.source] ?? "\u6458\u8981";
150462
150641
  if (persistBase) {
150463
- const base2 = await ctx.deps.sessionMap.getRootCard(sessionID);
150464
150642
  if (base2) {
150465
150643
  await ctx.deps.sessionMap.setRootCard(sessionID, { ...base2, summaryPending: false, summary, summaryLabel });
150466
150644
  }
@@ -150754,6 +150932,19 @@ var SessionCommands = class {
150754
150932
  );
150755
150933
  this.ctx = ctx;
150756
150934
  }
150935
+ /** AI 管理台:构建会话列表卡(不发送;调用方自行 patch / 发送)。 */
150936
+ async buildSessionListCard(chatId, page = 0) {
150937
+ const entries3 = await this.ctx.loadSessionEntries(chatId);
150938
+ const active = await this.ctx.deps.sessionMap.getActive(chatId);
150939
+ return this.ctx.buildListCard(chatId, entries3, page, active?.sessionID);
150940
+ }
150941
+ /**
150942
+ * AI 管理台:构建 AI 预填的建会话表单卡(写入向导状态,**不发送**)。
150943
+ * 表单消息 id 即后续话题锚点;用户确认/修改后提交走既有表单流程。
150944
+ */
150945
+ async buildPrefilledSetupForm(chatId, anchorMessageId, prefill) {
150946
+ return this.ctx.buildPrefilledSetupForm(chatId, anchorMessageId, prefill);
150947
+ }
150757
150948
  /**
150758
150949
  * 文本命令拦截。返回 true 表示已作为命令处理(调用方**不得**再发 prompt)。
150759
150950
  * 非命令返回 false。
@@ -150908,6 +151099,7 @@ async function routeEvent(event, deps) {
150908
151099
  break;
150909
151100
  case "session.text.started": {
150910
151101
  const data = event.data;
151102
+ deps.log.debug("text.started", { sessionID: data.sessionID, msg: data.assistantMessageID });
150911
151103
  deps.applyRun(data.sessionID, {
150912
151104
  type: "text.started",
150913
151105
  ...data.assistantMessageID ? { assistantMessageID: data.assistantMessageID } : {}
@@ -150916,6 +151108,11 @@ async function routeEvent(event, deps) {
150916
151108
  }
150917
151109
  case "session.text.delta": {
150918
151110
  const data = event.data;
151111
+ deps.log.debug("text.delta", {
151112
+ sessionID: data.sessionID,
151113
+ msg: data.assistantMessageID,
151114
+ len: typeof data.delta === "string" ? data.delta.length : 0
151115
+ });
150919
151116
  deps.applyRun(data.sessionID, {
150920
151117
  type: "text.delta",
150921
151118
  delta: data.delta,
@@ -150925,6 +151122,12 @@ async function routeEvent(event, deps) {
150925
151122
  }
150926
151123
  case "session.text.ended": {
150927
151124
  const data = event.data;
151125
+ deps.log.debug("text.ended", {
151126
+ sessionID: data.sessionID,
151127
+ msg: data.assistantMessageID,
151128
+ len: typeof data.text === "string" ? data.text.length : -1,
151129
+ hasText: typeof data.text === "string"
151130
+ });
150928
151131
  deps.applyRun(data.sessionID, {
150929
151132
  type: "text.ended",
150930
151133
  ...data.text ? { text: data.text } : {},
@@ -151502,6 +151705,7 @@ async function compactSessionHttp(sessionID, directory, deps) {
151502
151705
  }
151503
151706
 
151504
151707
  // src/session/quick-generate.ts
151708
+ var TEMP_SESSION_TITLE = "\u2699\uFE0F \u5185\u90E8\u751F\u6210\uFF08\u4E34\u65F6\uFF0C\u53EF\u5FFD\u7565\uFF09";
151505
151709
  function sessionRoutingHeaders(sessionID, directory) {
151506
151710
  const headers = { "x-opencode-session": sessionID };
151507
151711
  if (directory) headers["x-opencode-directory"] = encodeURIComponent(directory);
@@ -151512,12 +151716,38 @@ function authHeaders3(service3) {
151512
151716
  const token = Buffer.from(`opencode:${service3.password}`, "utf8").toString("base64");
151513
151717
  return { authorization: `Basic ${token}` };
151514
151718
  }
151719
+ function sessionChannelEnabled(value3) {
151720
+ if (value3 === "on") return true;
151721
+ if (value3 === "off") return false;
151722
+ const env = process.env;
151723
+ return env["VITEST"] === void 0 && env["NODE_ENV"] !== "test";
151724
+ }
151725
+ function httpChannelAllowed(deps) {
151726
+ if (deps.discover || deps.fetchImpl) return true;
151727
+ const env = process.env;
151728
+ return env["VITEST"] === void 0 && env["NODE_ENV"] !== "test";
151729
+ }
151515
151730
  async function quickGenerateWithSession(deps, input) {
151731
+ const httpAllowed = httpChannelAllowed(deps);
151732
+ const discover = deps.discover ?? (() => discoverLocalService());
151733
+ const service3 = httpAllowed ? await discover().catch(() => void 0) : void 0;
151734
+ if (service3 && sessionChannelEnabled(deps.sessionChannel)) {
151735
+ try {
151736
+ const result4 = await generateViaTemporarySession(deps, input, service3);
151737
+ deps.log.info("\u5FEB\u6458\u8981\u751F\u6210\u8D70\u4E34\u65F6\u4F1A\u8BDD", { sessionID: input.sessionID, route: "session" });
151738
+ return { route: "session", result: result4 };
151739
+ } catch (err2) {
151740
+ deps.log.debug("\u4E34\u65F6\u4F1A\u8BDD\u751F\u6210\u5931\u8D25\uFF0C\u56DE\u9000\u751F\u6210 API / \u672C\u673A HTTP", {
151741
+ sessionID: input.sessionID,
151742
+ error: errorMessage(err2)
151743
+ });
151744
+ }
151745
+ }
151516
151746
  const headers = sessionRoutingHeaders(input.sessionID, input.directory);
151517
151747
  if (deps.generateText) {
151518
151748
  try {
151519
151749
  const result4 = await withAttemptTimeout(
151520
- deps.generateText(input.prompt, { headers }),
151750
+ deps.generateText(input.prompt, { headers }, input.model),
151521
151751
  deps.attemptTimeoutMs ?? 8e3
151522
151752
  );
151523
151753
  if (result4 !== void 0 && result4 !== null) {
@@ -151532,14 +151762,20 @@ async function quickGenerateWithSession(deps, input) {
151532
151762
  });
151533
151763
  }
151534
151764
  }
151535
- const discover = deps.discover ?? (() => discoverLocalService());
151536
151765
  const doFetch = deps.fetchImpl ?? fetch;
151537
- const service3 = await discover();
151538
- if (!service3) throw new Error("\u751F\u6210 API \u4E0E HTTP \u901A\u9053\u5747\u4E0D\u53EF\u7528\uFF08\u672A\u53D1\u73B0\u672C\u673A opencode \u670D\u52A1\u6CE8\u518C\uFF09");
151766
+ if (!service3) {
151767
+ throw new Error(
151768
+ httpAllowed ? "\u751F\u6210 API \u4E0E HTTP \u901A\u9053\u5747\u4E0D\u53EF\u7528\uFF08\u672A\u53D1\u73B0\u672C\u673A opencode \u670D\u52A1\u6CE8\u518C\uFF09" : "\u6D4B\u8BD5\u73AF\u5883\u5DF2\u7981\u7528\u672C\u673A HTTP \u751F\u6210\u901A\u9053\uFF08\u672A\u663E\u5F0F\u6CE8\u5165 discover/fetchImpl\uFF09"
151769
+ );
151770
+ }
151539
151771
  const response2 = await doFetch(`${service3.url}/api/experimental/generate`, {
151540
151772
  method: "POST",
151541
151773
  headers: { "content-type": "application/json", ...authHeaders3(service3), ...headers },
151542
- body: JSON.stringify({ prompt: input.prompt }),
151774
+ body: JSON.stringify({
151775
+ prompt: input.prompt,
151776
+ // 显式模型:未指定时服务端依赖"受支持的默认模型",实际环境常见 400。
151777
+ ...input.model ? { model: input.model } : {}
151778
+ }),
151543
151779
  signal: AbortSignal.timeout(deps.timeoutMs ?? 15e3)
151544
151780
  });
151545
151781
  if (!response2.ok) {
@@ -151553,6 +151789,56 @@ async function quickGenerateWithSession(deps, input) {
151553
151789
  deps.log.info("\u5FEB\u6458\u8981\u751F\u6210\u8D70\u672C\u673A HTTP \u751F\u6210", { sessionID: input.sessionID, route: "http" });
151554
151790
  return { route: "http", result: result3 };
151555
151791
  }
151792
+ async function generateViaTemporarySession(deps, input, service3) {
151793
+ const doFetch = deps.fetchImpl ?? fetch;
151794
+ const base2 = service3.url.replace(/\/+$/, "");
151795
+ const auth = authHeaders3(service3);
151796
+ const dirHeader = input.directory ? { "x-opencode-directory": encodeURIComponent(input.directory) } : {};
151797
+ const timeout3 = deps.timeoutMs ?? 15e3;
151798
+ const created = await doFetch(`${base2}/api/session`, {
151799
+ method: "POST",
151800
+ headers: { "content-type": "application/json", ...auth, ...dirHeader },
151801
+ body: JSON.stringify({
151802
+ title: TEMP_SESSION_TITLE,
151803
+ ...input.model ? { model: input.model } : {},
151804
+ ...input.directory ? { location: { directory: input.directory } } : {}
151805
+ }),
151806
+ signal: AbortSignal.timeout(timeout3)
151807
+ });
151808
+ if (!created.ok) {
151809
+ const detail = await created.text().catch(() => "");
151810
+ throw new Error(`\u4E34\u65F6\u4F1A\u8BDD\u521B\u5EFA\u5931\u8D25 HTTP ${created.status}${detail ? `: ${detail.slice(0, 160)}` : ""}`);
151811
+ }
151812
+ const tempID = extractSessionID(await created.json());
151813
+ if (!tempID) throw new Error("\u4E34\u65F6\u4F1A\u8BDD\u521B\u5EFA\u54CD\u5E94\u7F3A\u5C11\u4F1A\u8BDD id");
151814
+ try {
151815
+ const generated = await doFetch(`${base2}/api/session/${encodeURIComponent(tempID)}/generate`, {
151816
+ method: "POST",
151817
+ headers: { "content-type": "application/json", ...auth, ...dirHeader },
151818
+ body: JSON.stringify({ prompt: input.prompt }),
151819
+ signal: AbortSignal.timeout(timeout3)
151820
+ });
151821
+ if (!generated.ok) {
151822
+ const detail = await generated.text().catch(() => "");
151823
+ throw new Error(`\u4E34\u65F6\u4F1A\u8BDD\u751F\u6210\u5931\u8D25 HTTP ${generated.status}${detail ? `: ${detail.slice(0, 160)}` : ""}`);
151824
+ }
151825
+ return await generated.json();
151826
+ } finally {
151827
+ await doFetch(`${base2}/api/session/${encodeURIComponent(tempID)}`, {
151828
+ method: "DELETE",
151829
+ headers: { ...auth, ...dirHeader },
151830
+ signal: AbortSignal.timeout(5e3)
151831
+ }).catch(() => void 0);
151832
+ }
151833
+ }
151834
+ function extractSessionID(raw) {
151835
+ if (typeof raw !== "object" || raw === null) return void 0;
151836
+ const record2 = raw;
151837
+ const data = record2["data"];
151838
+ const fromData = typeof data === "object" && data !== null ? data["id"] : void 0;
151839
+ const id2 = typeof record2["id"] === "string" ? record2["id"] : fromData;
151840
+ return typeof id2 === "string" && id2.startsWith("ses") ? id2 : void 0;
151841
+ }
151556
151842
  function withAttemptTimeout(promise3, ms) {
151557
151843
  return new Promise((resolve4, reject) => {
151558
151844
  const timer = setTimeout(() => reject(new Error("generate-attempt-timeout")), Math.max(1, ms));
@@ -151815,6 +152101,12 @@ async function start(ctx, config, log2, logSink) {
151815
152101
  return void 0;
151816
152102
  }
151817
152103
  }
152104
+ async function resolveGenerateModel(sessionID, directory) {
152105
+ const sessionModel = await readSessionModelQuiet(sessionID, directory);
152106
+ if (sessionModel) return { providerID: sessionModel.providerID, id: sessionModel.id };
152107
+ const first = (await listModels())[0];
152108
+ return first ? { providerID: first.providerID, id: first.id } : void 0;
152109
+ }
151818
152110
  async function switchSessionModel(sessionID, model) {
151819
152111
  const link2 = await sessionMap.resolveBySession(sessionID);
151820
152112
  const options2 = link2?.dir ? { headers: { "x-opencode-directory": link2.dir } } : void 0;
@@ -151917,14 +152209,20 @@ async function start(ctx, config, log2, logSink) {
151917
152209
  const readMessages = async (sessionID, directory) => readSessionMessages(ctx, sessionID, directory, log2);
151918
152210
  const generateText = async (prompt, directory) => {
151919
152211
  const api = ctx.generate.text;
152212
+ const model = await resolveGenerateModel(input.sessionID, directory);
151920
152213
  const outcome = await quickGenerateWithSession(
151921
152214
  {
151922
152215
  log: log2,
151923
152216
  ...typeof api === "function" ? {
151924
- generateText: (p, requestOptions) => api({ prompt: p }, requestOptions)
152217
+ generateText: (p, requestOptions, modelRef) => api({ prompt: p, ...modelRef ? { model: modelRef } : {} }, requestOptions)
151925
152218
  } : {}
151926
152219
  },
151927
- { prompt, sessionID: input.sessionID, ...directory ? { directory } : {} }
152220
+ {
152221
+ prompt,
152222
+ sessionID: input.sessionID,
152223
+ ...directory ? { directory } : {},
152224
+ ...model ? { model } : {}
152225
+ }
151928
152226
  );
151929
152227
  return outcome.result;
151930
152228
  };
@@ -152088,6 +152386,10 @@ async function start(ctx, config, log2, logSink) {
152088
152386
  rootKnown: Boolean(rootLink)
152089
152387
  });
152090
152388
  if (decision.kind === "main-hint") {
152389
+ if (config.quickNew) {
152390
+ await handleQuickNew(message);
152391
+ return;
152392
+ }
152091
152393
  const res = await sender.sendCard(message.chatId, buildConsoleHintCard());
152092
152394
  if (!res.ok) log2.warn("\u7BA1\u7406\u53F0\u63D0\u793A\u5361\u53D1\u9001\u5931\u8D25", { error: res.error ?? "unknown" });
152093
152395
  return;
@@ -152183,6 +152485,114 @@ async function start(ctx, config, log2, logSink) {
152183
152485
  log2.info("\u673A\u5668\u4EBA\u83DC\u5355\u70B9\u51FB \u2192 \u6267\u884C\u547D\u4EE4", { eventKey: click.eventKey, cmd: entry.cmd, label: entry.label });
152184
152486
  await handleMessage(synthetic);
152185
152487
  }
152488
+ async function handleQuickNew(message) {
152489
+ const hint = async () => {
152490
+ const res = await sender.sendCard(message.chatId, buildConsoleHintCard());
152491
+ if (!res.ok) log2.warn("\u7BA1\u7406\u53F0\u63D0\u793A\u5361\u53D1\u9001\u5931\u8D25", { error: res.error ?? "unknown" });
152492
+ };
152493
+ try {
152494
+ const candidates = [];
152495
+ const seen = /* @__PURE__ */ new Set();
152496
+ for (const dir2 of await recent.listDirs()) {
152497
+ if (!seen.has(dir2)) {
152498
+ seen.add(dir2);
152499
+ candidates.push({ path: dir2 });
152500
+ }
152501
+ }
152502
+ let entries3 = [];
152503
+ try {
152504
+ const raw = await listAllSessionsRaw() ?? await listSessionsOverHttp({}, { log: log2 });
152505
+ entries3 = normalizeSessionList(raw) ?? [];
152506
+ } catch (err2) {
152507
+ log2.debug("quick-new \u4F1A\u8BDD\u5217\u8868\u8BFB\u53D6\u5931\u8D25", { error: errorMessage(err2) });
152508
+ }
152509
+ for (const entry of entries3) {
152510
+ if (entry.directory && !seen.has(entry.directory)) {
152511
+ seen.add(entry.directory);
152512
+ candidates.push({ path: entry.directory, ...entry.title ? { label: entry.title } : {} });
152513
+ }
152514
+ }
152515
+ const routingSessionID = entries3[0]?.sessionID;
152516
+ if (!routingSessionID) {
152517
+ log2.debug("quick-new \u65E0\u53EF\u7528\u4F1A\u8BDD\uFF08\u751F\u6210\u901A\u9053\u7F3A\u5C11\u8DEF\u7531 sessionID\uFF09\uFF0C\u56DE\u9000\u63D0\u793A\u5361");
152518
+ await hint();
152519
+ return;
152520
+ }
152521
+ let models = [];
152522
+ try {
152523
+ models = (await listModels()).map((m) => ({
152524
+ providerID: m.providerID,
152525
+ id: m.id,
152526
+ ...m.name ? { name: m.name } : {}
152527
+ }));
152528
+ } catch (err2) {
152529
+ log2.debug("quick-new \u6A21\u578B\u5217\u8868\u8BFB\u53D6\u5931\u8D25", { error: errorMessage(err2) });
152530
+ }
152531
+ const ack = await sender.sendCard(message.chatId, buildQuickNewThinkingCard());
152532
+ if (!ack.ok || !ack.messageId) {
152533
+ await hint();
152534
+ return;
152535
+ }
152536
+ const genModel = await resolveGenerateModel(routingSessionID, entries3[0]?.directory);
152537
+ const generateText = async (prompt, requestOptions, model2) => {
152538
+ const api = ctx.generate.text;
152539
+ if (typeof api !== "function") throw new Error("generate.text unavailable");
152540
+ return api({ prompt, ...model2 ? { model: model2 } : {} }, requestOptions);
152541
+ };
152542
+ const outcome = await quickGenerateWithSession(
152543
+ { log: log2, generateText },
152544
+ {
152545
+ prompt: buildQuickNewPrompt({ text: message.text, candidates, models }),
152546
+ sessionID: routingSessionID,
152547
+ ...genModel ? { model: genModel } : {}
152548
+ }
152549
+ );
152550
+ const decision = parseQuickNewDecision(extractGeneratedText(outcome.result));
152551
+ log2.debug("quick-new \u8BC6\u522B\u7ED3\u679C", {
152552
+ intent: decision?.intent,
152553
+ dir: decision?.directory,
152554
+ perm: decision?.perm,
152555
+ model: decision?.model,
152556
+ reason: decision?.reason
152557
+ });
152558
+ if (!decision || decision.intent === "chat") {
152559
+ await sender.patchCard(ack.messageId, buildConsoleHintCard());
152560
+ return;
152561
+ }
152562
+ if (decision.intent === "list") {
152563
+ const card = await commands.buildSessionListCard(message.chatId);
152564
+ await sender.patchCard(ack.messageId, card);
152565
+ log2.info("quick-new\uFF1A\u8BC6\u522B\u5361\u5DF2\u5C31\u5730\u5207\u6362\u4E3A\u4F1A\u8BDD\u5217\u8868\u5361", { chatId: message.chatId });
152566
+ return;
152567
+ }
152568
+ const dir = matchCandidateDirectory(decision.directory, candidates);
152569
+ const validation = dir ? validateDir(dir) : void 0;
152570
+ const model = matchModelOption(decision.model, models);
152571
+ const prefill = {
152572
+ title: decision.title || topicTitle(message.text),
152573
+ ...validation?.ok ? { dir: validation.path } : {},
152574
+ ...model ? {
152575
+ model: {
152576
+ providerID: model.providerID,
152577
+ id: model.id,
152578
+ ...model.name ? { name: model.name } : {}
152579
+ }
152580
+ } : {},
152581
+ ...decision.perm ? { perm: decision.perm } : {}
152582
+ };
152583
+ const form = await commands.buildPrefilledSetupForm(message.chatId, ack.messageId, prefill);
152584
+ await sender.patchCard(ack.messageId, form);
152585
+ log2.info("quick-new\uFF1AAI \u9884\u586B\u5EFA\u4F1A\u8BDD\u8868\u5355\u5DF2\u53D1\u9001\uFF08\u5C31\u5730\uFF09", {
152586
+ title: prefill.title,
152587
+ dir: validation?.ok ? validation.path : void 0,
152588
+ perm: decision.perm,
152589
+ model: model ? `${model.providerID}/${model.id}` : void 0
152590
+ });
152591
+ } catch (err2) {
152592
+ log2.warn("quick-new \u5904\u7406\u5931\u8D25\uFF0C\u56DE\u9000\u7BA1\u7406\u53F0\u63D0\u793A", { error: errorMessage(err2) });
152593
+ await hint();
152594
+ }
152595
+ }
152186
152596
  async function runInActiveSession(message) {
152187
152597
  let active = await sessionMap.getActive(message.chatId);
152188
152598
  if (!active) {
@@ -152199,7 +152609,7 @@ async function start(ctx, config, log2, logSink) {
152199
152609
  await runInSession(message, active.sessionID);
152200
152610
  }
152201
152611
  async function runInSession(message, sessionID, replyToMessageId, forceDelivery) {
152202
- const delivery = forceDelivery ?? decideDelivery(executions.isRunning(sessionID));
152612
+ const delivery = forceDelivery ?? decideDelivery(executions.isRunning(sessionID), config.busyDelivery);
152203
152613
  const link2 = await sessionMap.resolveBySession(sessionID);
152204
152614
  const modelRef = await readSessionModelQuiet(sessionID, link2?.dir) ?? link2?.model;
152205
152615
  const model = modelRef ? modelLabel(modelRef) : void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-feishu-plugin",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "Feishu/Lark plugin for OpenCode V2 — single-user topics as sessions, in-card permission approvals, streaming cards. Minimal permissions, no public endpoint.",
5
5
  "type": "module",
6
6
  "main": "index.js",