opencode-feishu-plugin 0.2.14 → 0.2.16

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,7 +20,7 @@ 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
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 |
26
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 |
@@ -128,21 +128,28 @@ After configuring the menu (setup step 6), two quick buttons stay pinned **above
128
128
  - After changing the menu, **re-publish the app version** (per the console: takes effect within ~5 minutes after publishing);
129
129
  - Message the bot at least once before using the menu (the plugin remembers the p2p chat from messages; any normal usage satisfies this).
130
130
 
131
- ### One-sentence session creation (AI finds the directory)
131
+ ### AI session management (main chat)
132
132
 
133
- When you send plain text in the main chat, the AI first judges the **intent** and picks the best-matching working directory from candidates (recent dirs + local session dirs), then replies with a proposal card:
133
+ Send plain text in the main chat and the AI judges the intent and handles it:
134
134
 
135
- ```
136
- You: fix the zlib download bug
137
- → 🆕 Suggested new session
138
- Task: fix download bug | Dir: `/Users/code/zlib`
139
- [✅ Create & send] [❌ Cancel]
140
- ```
135
+ - **Create a session (directory first)**: the AI first determines the **working directory**, then turns the card in place into a **prefilled form** — confirm (or tweak) and tap "✅ 创建会话" to create the session and auto-open a topic:
136
+
137
+ ```
138
+ You: fix the zlib download bug, use high-risk approval
139
+ → 📝 Create-session form (dir `/Users/code/zlib` ✓ matched an existing dir; permission "High-risk approval")
140
+ [✅ 创建会话]
141
+
142
+ You: stock research
143
+ → 📝 Create-session form (dir `/Users/code/stock-research` ➕ AI-created; auto-created on submit)
144
+ [✅ 创建会话]
145
+ ```
141
146
 
142
- "Create & send" = create the session + auto-open a topic + send your original message in to start working (permission defaults to "Editable"; adjust later with `/perm`).
147
+ Directory decision order: ① a path you gave explicitly → ② semantic match against an **existing directory** (the AI looks at the **allowed root's first-level subdirectories first**, then recent / session dirs) → ③ otherwise **create a new one under an allowed root** (`<allowed root>/<kebab-case topic>`) → ④ fall back to the allowed root. **The form always carries a directory** — never empty.
143
148
 
144
- - Chat/unrecognized/dir-not-in-candidates → falls back to the console hint card, **never creates a wrong session**; the model's path must match a candidate (anti-hallucination);
145
- - Proposal cards are valid for 1 hour; set `quickNew: false` to go back to the console-only mode.
149
+ - **List sessions**: say "what sessions do I have?" → a session list card appears directly (same as `/sessions`; paginate / enter / create);
150
+ - **Chit-chat / other**: the usual console hint card.
151
+
152
+ Guardrails: AI-proposed paths must fall under the allowed roots (`allowedRoots`) and models must be from the available list; a path **you** gave that is out of range is **never silently replaced** — the form warns and lets you fix it. Creation always goes through form confirmation. `quickNew: false` disables the whole thing.
146
153
 
147
154
  ### Inside a topic (work)
148
155
 
@@ -192,6 +199,7 @@ When the agent asks via `question` or other form tools, the form becomes a Feish
192
199
  | `approvalTtlMs` | number | `600000` | Approval token / card validity |
193
200
  | `staleExecutionMs` | number | `300000` | Watchdog threshold (0–60 min; **0 = disabled**; sessions waiting on a form / pending approval are never killed) |
194
201
  | `quickNew` | boolean | `true` | Main-chat "one-sentence session": AI judges intent + finds the directory, proposal card creates in one tap; `false` = console-only mode |
202
+ | `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) |
195
203
  | `gatewayLocation` | string | — | Only start the gateway at this location (or its subdirectories); empty = any location |
196
204
 
197
205
  Full config (including `cardMaxTables`, `topicStatus*`, `resumeSummary*`, `keepalive*`, `gatewayMatchGraceMs`) → [docs/advanced.en.md](./docs/advanced.en.md#full-configuration).
@@ -225,7 +233,23 @@ Full config (including `cardMaxTables`, `topicStatus*`, `resumeSummary*`, `keepa
225
233
  Iterating from real usage feedback; current plan:
226
234
 
227
235
  - [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).
228
- - [ ] **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.
236
+ - [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).
237
+
238
+ ## 8. Changelog
239
+
240
+ | Version | Highlights |
241
+ |---|---|
242
+ | **v0.2.16** | Session creation is **directory-first**: the AI pins the working directory before prefilling the form, so the form never has an empty directory; directory candidates now include the **first-level subdirectories of allowed roots**, preferring an existing directory before creating a new one |
243
+ | **v0.2.15** | **AI session management** (the main chat lets the AI judge intent and return a prefilled form / session list); **new messages cut in by default when busy** (`busyDelivery: "queue"` to queue instead); fix transient generation always failing under opencode-go |
244
+ | **v0.2.14** | **One-sentence session creation**; fix duplicated / overlapping streaming text |
245
+ | **v0.2.13** | Fix the queued receipt card stuck on "waiting" after its terminal state; add execution-event diagnostics |
246
+ | **v0.2.12** | Fix watchdog false kills (no longer kills while a subagent / question / approval is pending); the watchdog can be disabled |
247
+ | **v0.2.11** | **Bot custom menu** (quick `/new`, `/sessions` buttons above the input); support the SDK's flattened event shape |
248
+ | **v0.2.10** | Attachments now land in the session working directory `.opencode/temp/opencode-feishu-plugin/` (with built-in `.gitignore`) |
249
+ | **v0.2.9** | **Receive images / files** (downloaded and attached to the session; requires `im:message:readonly`) |
250
+ | **v0.2.8** | **Event subscription auto-reconnect** (exponential backoff on stream drop, no more permanent blindness); auto-open a topic when resuming a session; fallback summary extraction |
251
+
252
+ Full history: [GitHub Releases](https://github.com/moyuanhua/opencode-feishu-plugin/releases).
229
253
 
230
254
  ## Advanced topics & development
231
255
 
package/README.md CHANGED
@@ -20,7 +20,7 @@
20
20
  | 🚀 **一键建会话** | `/new` 一张表单(目录 + 模型 + 权限档位)一次填完,提交即建会话并自动开话题 |
21
21
  | ✅ **卡片审批** | 权限请求变飞书卡片:允许一次 / 始终允许 / 本会话内允许 / 拒绝,自签 token 防伪防重放 |
22
22
  | 📊 **实时可见** | 「思考中」回执 → 工具调用实时上卡 → 文本流式更新,页脚显示当前模型 |
23
- | ⏹ **可控可停** | 每张回复卡带「强制停止」;看门狗自动中断卡死会话;忙时原生排队,`/steer` `/now` 可插队 |
23
+ | ⏹ **可控可停** | 每张回复卡带「强制停止」;看门狗自动中断卡死会话;忙时新消息**默认插队**(可配置为排队),`/steer` `/now` 随时可用 |
24
24
  | 📎 **图片 / 文件** | 飞书里的图片 / 文件自动下载并挂进会话,支持视觉 / 文件的模型直接看图、读文件 |
25
25
  | 🧭 **一句话建会话** | 主聊天流发任务文本,AI 自动判断意图、找好工作目录,一键建会话并开始处理 |
26
26
  | 🖱 **主窗口菜单** | 机器人输入框上固定「新建会话」「会话列表」快捷按钮(机器人自定义菜单),一点即用 |
@@ -128,21 +128,28 @@ tail -f ~/.config/opencode/plugins/feishu.log # 应看到「飞书长连接已
128
128
  - 菜单配置修改后需**重新发布版本**(官方说明:发版成功后约 5 分钟内生效);
129
129
  - 首次使用菜单前,先给机器人发过至少一条消息(插件据消息记住单聊会话;正常使用必然满足)。
130
130
 
131
- ### 一句话建会话(AI 找目录)
131
+ ### AI 会话管理(主聊天流)
132
132
 
133
- 主聊天流发普通文本时,AI 会先判断**意图**,并从候选目录(最近使用 + 本机会话目录)里挑出最匹配的工作目录,回一张建议卡:
133
+ 主聊天流发普通文本,AI 会判断意图并直接处理:
134
134
 
135
- ```
136
- 你:帮我修一下 zlib 的下载 bug
137
- → 🆕 建议新建会话
138
- 任务:修下载 bug | 目录:`/Users/code/zlib`
139
- [✅ 创建并发送] [❌ 取消]
140
- ```
135
+ - **建会话(目录优先)**:AI 先把**工作目录定下来**,再就地给出**预填表单**——确认或微调后点「✅ 创建会话」即可,自动开话题开工:
136
+
137
+ ```
138
+ 你:帮我修一下 zlib 的下载 bug,用高风险审批
139
+ → 📝 建会话表单(目录 `/Users/code/zlib` ✓ 匹配历史目录;权限「高风险审批」)
140
+ [✅ 创建会话]
141
+
142
+ 你:股票研究
143
+ → 📝 建会话表单(目录 `/Users/code/stock-research` ➕ AI 新建,不存在时会在创建时自动创建)
144
+ [✅ 创建会话]
145
+ ```
141
146
 
142
- 点「创建并发送」= 建会话 + 自动开话题 + 把原消息发进去开始处理(权限默认「可编辑」,进话题后可用 `/perm` 调整)。
147
+ 目录决策顺序:① 你明确给的路径 → ② 语义匹配**现成目录**(AI 会先看**允许根目录的一级子目录**,再看最近使用 / 历史会话目录)→ ③ 都不匹配则**按主题在允许根目录下新建**(`<允许根目录>/<英文短横线主题>`)→ ④ 兜底允许根目录。**表单永远带目录**,不会出现空目录。
143
148
 
144
- - 闲聊 / 无法识别 / 目录不在候选中 → 回退为管理台提示卡,**不会误建会话**;模型给出的路径必须命中候选清单(防幻觉);
145
- - 建议卡有效期 1 小时;不想要此功能可设 `quickNew: false` 回到纯管理台模式。
149
+ - **列会话**:说「我有哪些会话」→ 直接出会话列表卡(等同 `/sessions`,可翻页 / 进话题 / 新建);
150
+ - **闲聊 / 其它**:照旧回管理台提示卡。
151
+
152
+ 防误伤:AI 新造的路径必须落在允许范围(`allowedRoots`)内、模型必须命中可选列表;**你明确指定但越界的路径不会静默替换**——表单不预填并给出警示,由你修改。最终一定经过表单确认。`quickNew: false` 可整体关闭。
146
153
 
147
154
  ### 话题内(干活)
148
155
 
@@ -192,6 +199,7 @@ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发
192
199
  | `approvalTtlMs` | number | `600000` | 审批 token / 卡片有效期 |
193
200
  | `staleExecutionMs` | number | `300000` | 看门狗阈值(0–60 分钟;**0 = 关闭看门狗**;待答表单 / 未决审批期间不判卡死) |
194
201
  | `quickNew` | boolean | `true` | 主聊天流「一句话建会话」:AI 判断意图 + 找目录,建议卡一键创建;`false` = 回到纯管理台模式 |
202
+ | `busyDelivery` | `steer`\|`queue` | `steer` | 忙时新消息投递方式:`steer` = 立即插队打断当前步骤;`queue` = 原生排队(长命令场景更温和) |
195
203
  | `gatewayLocation` | string | — | 只在该 location(及其子目录)启动网关;留空 = 任意 location 生效 |
196
204
 
197
205
  完整配置(含 `cardMaxTables`、`topicStatus*`、`resumeSummary*`、`keepalive*`、`gatewayMatchGraceMs` 等进阶项)见 [docs/advanced.md](./docs/advanced.md#完整配置项)。
@@ -225,7 +233,23 @@ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发
225
233
  按真实使用反馈迭代,当前规划:
226
234
 
227
235
  - [x] **接收图片 / 文件**:已支持——自动下载到**会话工作目录**下的 `.opencode/temp/opencode-feishu-plugin/`(内置 `.gitignore`,不污染 `git status`;可用 `attachmentsDir` 覆盖;单附件默认 ≤20MB)。
228
- - [ ] **忙时新消息默认插队**:目前会话忙时新消息默认排队(可用 `/steer`、`/now` 手动插队)。规划:忙时你发的新消息**默认直接插队**,立即打断当前步骤优先执行。
236
+ - [x] **忙时新消息默认插队**:已支持——忙碌时新消息**默认直接插队**(打断当前步骤优先执行);`busyDelivery: "queue"` 可切回原生排队(长命令场景更温和)。
237
+
238
+ ## 八、更新日志
239
+
240
+ | 版本 | 亮点 |
241
+ |---|---|
242
+ | **v0.2.16** | 建会话**目录优先**:AI 先定好工作目录再预填表单,表单永不空目录;目录候选新增「允许根目录的一级子目录」,先复用现成目录,找不到才按主题新建 |
243
+ | **v0.2.15** | **AI 会话管理**(主聊天流由 AI 判意图,直接给预填表单 / 会话列表);**忙时默认插队**(`busyDelivery: "queue"` 可切回排队);修复 opencode-go 下临时生成必失败 |
244
+ | **v0.2.14** | **一句话建会话**;修复流式文本重复 / 跨段重叠 |
245
+ | **v0.2.13** | 修复排队回执卡终态后永久「等待中」;补充执行事件诊断日志 |
246
+ | **v0.2.12** | 修复看门狗误杀(子代理 / 待答 / 未决审批期间不再判卡死);看门狗可关闭 |
247
+ | **v0.2.11** | **机器人自定义菜单**(输入框快捷按钮 `/new`、`/sessions`);兼容 SDK 拍平的事件形状 |
248
+ | **v0.2.10** | 附件默认落盘到会话工作目录 `.opencode/temp/opencode-feishu-plugin/`(内置 `.gitignore`) |
249
+ | **v0.2.9** | **接收图片 / 文件**(自动下载并挂进会话,需 `im:message:readonly`) |
250
+ | **v0.2.8** | **事件订阅自动重连**(SSE 断流按指数退避重连,不再永久失明);进历史会话自动开话题;摘要截取兜底 |
251
+
252
+ 完整历史见 [GitHub Releases](https://github.com/moyuanhua/opencode-feishu-plugin/releases)。
229
253
 
230
254
  ## 高级主题与开发
231
255
 
package/dist/index.js CHANGED
@@ -144038,6 +144038,7 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
144038
144038
  const attachmentTimeoutMs = clamp4(asNumber(merged.attachmentTimeoutMs, 3e4), 5e3, 12e4);
144039
144039
  const attachmentsDirRaw = asString(merged.attachmentsDir).trim();
144040
144040
  const quickNew = asBoolean(merged.quickNew, true);
144041
+ const busyDelivery = merged.busyDelivery === "queue" ? "queue" : "steer";
144041
144042
  const keepaliveIntervalMs = clamp4(
144042
144043
  asNumber(merged.keepaliveIntervalMs, 20 * 60 * 1e3),
144043
144044
  5 * 60 * 1e3,
@@ -144103,7 +144104,8 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
144103
144104
  attachmentMaxBytes,
144104
144105
  attachmentTimeoutMs,
144105
144106
  ...attachmentsDirRaw ? { attachmentsDir: attachmentsDirRaw } : {},
144106
- quickNew
144107
+ quickNew,
144108
+ busyDelivery
144107
144109
  };
144108
144110
  }
144109
144111
  function loadConfigFile(env, deps, warnings) {
@@ -145932,8 +145934,8 @@ function readTimestamp(value3) {
145932
145934
  }
145933
145935
 
145934
145936
  // src/feishu/delivery.ts
145935
- function decideDelivery(running) {
145936
- return running ? "queue" : "steer";
145937
+ function decideDelivery(running, busyDelivery = "queue") {
145938
+ return running ? busyDelivery : "steer";
145937
145939
  }
145938
145940
  var ExecutionTracker = class {
145939
145941
  /** sessionID → 最近一次活动时间戳(ms)。 */
@@ -147955,6 +147957,7 @@ function decideRoute(facts) {
147955
147957
  }
147956
147958
 
147957
147959
  // src/feishu/session-list.ts
147960
+ var INTERNAL_SESSION_TITLE_PREFIX = "\u2699\uFE0F \u5185\u90E8\u751F\u6210\uFF08\u4E34\u65F6";
147958
147961
  function normalizeSessionList(raw) {
147959
147962
  const array3 = extractArray(raw);
147960
147963
  if (!array3) return void 0;
@@ -147963,6 +147966,7 @@ function normalizeSessionList(raw) {
147963
147966
  for (const item of array3) {
147964
147967
  const entry = normalizeSessionInfo(item);
147965
147968
  if (!entry || seen.has(entry.sessionID)) continue;
147969
+ if (entry.title.startsWith(INTERNAL_SESSION_TITLE_PREFIX)) continue;
147966
147970
  seen.add(entry.sessionID);
147967
147971
  out.push(entry);
147968
147972
  }
@@ -148564,93 +148568,67 @@ function parsePage(value3) {
148564
148568
  }
148565
148569
 
148566
148570
  // src/feishu/quick-new-cards.ts
148567
- function button3(text, type, value3) {
148568
- return {
148569
- tag: "button",
148570
- text: { tag: "plain_text", content: text },
148571
- type,
148572
- behaviors: [{ type: "callback", value: value3 }]
148573
- };
148574
- }
148575
148571
  function buildQuickNewThinkingCard() {
148576
148572
  return {
148577
148573
  schema: "2.0",
148578
148574
  config: { update_multi: true },
148579
- header: { title: { tag: "plain_text", content: "\u{1F914} \u6B63\u5728\u8BC6\u522B\u610F\u56FE\u4E0E\u76EE\u5F55\u2026" }, template: "blue" },
148575
+ header: { title: { tag: "plain_text", content: "\u{1F914} \u6B63\u5728\u8BC6\u522B\u610F\u56FE\u2026" }, template: "blue" },
148580
148576
  body: {
148581
148577
  elements: [
148582
- { tag: "markdown", content: "\u6B63\u5728\u5224\u65AD\u8FD9\u6761\u6D88\u606F\u662F\u5426\u9700\u8981\u65B0\u5EFA\u4F1A\u8BDD\uFF0C\u4EE5\u53CA\u6700\u5408\u9002\u7684\u5DE5\u4F5C\u76EE\u5F55\u2026" }
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
+ }
148583
148582
  ]
148584
148583
  }
148585
148584
  };
148586
148585
  }
148587
- function buildQuickNewProposalCard(input) {
148588
- const preview = input.textPreview.trim().replace(/\n+/g, " ").slice(0, 120);
148589
- const lines = [
148590
- `**\u4EFB\u52A1**\uFF1A${input.title}`,
148591
- `**\u76EE\u5F55**\uFF1A\`${input.directory}\``,
148592
- ...input.reason ? [`**\u4F9D\u636E**\uFF1A${input.reason}`] : [],
148593
- "",
148594
- `\u539F\u6D88\u606F\uFF1A${preview}`
148595
- ];
148596
- const elements = [
148597
- { tag: "markdown", content: truncateCardContent(lines.join("\n")) },
148598
- {
148599
- tag: "markdown",
148600
- content: "\u70B9\u300C\u521B\u5EFA\u5E76\u53D1\u9001\u300D= \u5EFA\u4F1A\u8BDD + \u81EA\u52A8\u5F00\u8BDD\u9898 + \u628A\u539F\u6D88\u606F\u53D1\u8FDB\u53BB\u5F00\u59CB\u5904\u7406\u3002",
148601
- text_size: "notation"
148602
- },
148603
- button3("\u2705 \u521B\u5EFA\u5E76\u53D1\u9001", "primary", { cmd: "quicknew", op: "create", id: input.id }),
148604
- button3("\u274C \u53D6\u6D88", "default", { cmd: "quicknew", op: "cancel", id: input.id })
148605
- ];
148606
- return {
148607
- schema: "2.0",
148608
- config: { update_multi: true },
148609
- header: { title: { tag: "plain_text", content: "\u{1F195} \u5EFA\u8BAE\u65B0\u5EFA\u4F1A\u8BDD" }, template: "turquoise" },
148610
- body: { elements }
148611
- };
148612
- }
148613
- function buildQuickNewResolvedCard(title, note2, template2 = "grey") {
148614
- return {
148615
- schema: "2.0",
148616
- config: { update_multi: true },
148617
- header: { title: { tag: "plain_text", content: title }, template: template2 },
148618
- body: { elements: [{ tag: "markdown", content: truncateCardContent(note2) }] }
148619
- };
148620
- }
148621
- function parseQuickNewActionValue(raw) {
148622
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
148623
- const value3 = raw;
148624
- if (value3.cmd !== "quicknew") return void 0;
148625
- const op = value3.op === "create" || value3.op === "cancel" ? value3.op : void 0;
148626
- const id2 = typeof value3.id === "string" ? value3.id.trim() : "";
148627
- if (!op || !id2) return void 0;
148628
- return { op, id: id2 };
148629
- }
148630
148586
 
148631
148587
  // src/session/quick-new.ts
148588
+ var QUICK_NEW_PERMS = ["readonly", "edit", "askHigh", "trust"];
148589
+ var QUICK_NEW_DIR_SOURCES = ["given", "existing", "new"];
148632
148590
  var QUICK_NEW_INSTRUCTION = [
148633
148591
  "\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",
148634
- "\u8BF7\u5224\u65AD\u5B83\u662F\u4E0D\u662F\u4E00\u4E2A\u9700\u8981\u65B0\u5EFA\u4F1A\u8BDD\u53BB\u6267\u884C\u7684\u5F00\u53D1/\u64CD\u4F5C\u4EFB\u52A1\uFF0C\u5E76\u4ECE\u5019\u9009\u76EE\u5F55\u4E2D\u9009\u51FA\u6700\u5408\u9002\u7684\u5DE5\u4F5C\u76EE\u5F55\u3002",
148635
- "\u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\uFF1A",
148636
- '{"intent":"task|chat","dir":"<\u7EDD\u5BF9\u8DEF\u5F84\u6216\u7A7A\u5B57\u7B26\u4E32>","title":"<\u4E0D\u8D85\u8FC720\u5B57\u7684\u4F1A\u8BDD\u6807\u9898>","reason":"<\u4E00\u53E5\u8BDD\u7406\u7531>"}',
148592
+ "\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",
148593
+ '{"intent":"create|list|chat","dir":"<\u7EDD\u5BF9\u8DEF\u5F84>","dir_source":"given|existing|new","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>"}',
148637
148594
  "\u89C4\u5219\uFF1A",
148638
- "- \u95F2\u804A\u3001\u95EE\u5019\u3001\u8BE2\u95EE\u7528\u6CD5 \u2192 intent=chat\uFF0Cdir \u7559\u7A7A\u3002",
148639
- "- \u660E\u786E\u7684\u5F00\u53D1/\u64CD\u4F5C\u4EFB\u52A1\uFF08\u6539 bug\u3001\u5199\u4EE3\u7801\u3001\u6574\u7406\u6587\u4EF6\u3001\u8DD1\u811A\u672C\u7B49\uFF09\u2192 intent=task\u3002",
148640
- "- 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"
148595
+ "- \u5217\u51FA/\u67E5\u770B\u4F1A\u8BDD \u2192 intent=list\uFF0C\u5176\u4F59\u5B57\u6BB5\u7559\u7A7A\u3002",
148596
+ "- \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",
148597
+ "- **\u76EE\u5F55\u89C4\u5219\uFF08create \u65F6 dir \u7EDD\u4E0D\u5141\u8BB8\u4E3A\u7A7A\uFF0C\u6309\u4F18\u5148\u7EA7\uFF09\uFF1A**",
148598
+ " \u5019\u9009\u76EE\u5F55\u5305\u62EC\uFF1A\u6700\u8FD1\u4F7F\u7528\u76EE\u5F55\u3001**\u5141\u8BB8\u6839\u76EE\u5F55\u7684\u4E00\u7EA7\u5B50\u76EE\u5F55**\u3001\u5386\u53F2\u4F1A\u8BDD\u76EE\u5F55\uFF08\u53EF\u80FD\u5E26\u6807\u9898\u7EBF\u7D22\uFF09\u3002",
148599
+ ' \u2460 \u7528\u6237\u6D88\u606F\u91CC\u660E\u786E\u7ED9\u4E86\u8DEF\u5F84 \u2192 dir=\u8BE5\u8DEF\u5F84\uFF0Cdir_source="given"\uFF1B',
148600
+ ' \u2461 \u5426\u5219\u5148\u770B\u5019\u9009\u91CC\u6709\u6CA1\u6709\u8BED\u4E49\u5339\u914D\u7684\u73B0\u6210\u76EE\u5F55\uFF08\u5C24\u5176\u5141\u8BB8\u6839\u76EE\u5F55\u7684\u4E00\u7EA7\u5B50\u76EE\u5F55\uFF09\u2192 dir=\u8BE5\u5019\u9009\u8DEF\u5F84\u539F\u6587\uFF0Cdir_source="existing"\uFF1B',
148601
+ ' \u2462 \u90FD\u4E0D\u5339\u914D\u624D\u65B0\u5EFA\uFF1Adir=<\u5141\u8BB8\u6839\u76EE\u5F55\u4E0B\u3001\u82F1\u6587\u5C0F\u5199\u77ED\u6A2A\u7EBF\u7684\u4E3B\u9898\u76EE\u5F55>\uFF08\u5982 /Users/code/stock-research\uFF09\uFF0Cdir_source="new"\uFF1B',
148602
+ ' \u2463 \u5B9E\u5728\u96BE\u4EE5\u547D\u540D \u2192 dir=<\u7B2C\u4E00\u4E2A\u5141\u8BB8\u6839\u76EE\u5F55>\uFF0Cdir_source="new"\u3002',
148603
+ "- 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",
148604
+ "- model \u53EA\u80FD\u4ECE\u5019\u9009\u6A21\u578B\u4E2D\u7CBE\u786E\u590D\u5236 providerID/modelID\uFF1B\u7528\u6237\u6CA1\u8BF4\u5C31\u7559\u7A7A\u3002"
148641
148605
  ].join("\n");
148642
- function buildQuickNewPrompt(text, candidates) {
148643
- const list = candidates.slice(0, 40).map((c) => `- ${c.path}${c.label ? `\uFF08${c.label}\uFF09` : ""}`).join("\n");
148606
+ function buildQuickNewPrompt(input) {
148607
+ const roots = (input.allowedRoots ?? []).slice(0, 8).map((r) => `- ${r}`).join("\n");
148608
+ const dirs = input.candidates.slice(0, 60).map((c) => `- ${c.path}${c.label ? `\uFF08${c.label}\uFF09` : ""}`).join("\n");
148609
+ const models = (input.models ?? []).slice(0, 30).map((m) => `- ${m.providerID}/${m.id}${m.name ? `\uFF08${m.name}\uFF09` : ""}`).join("\n");
148644
148610
  return [
148645
148611
  QUICK_NEW_INSTRUCTION,
148646
148612
  "",
148613
+ "\u5141\u8BB8\u6839\u76EE\u5F55\uFF08\u65B0\u5EFA\u76EE\u5F55\u65F6\u53EA\u80FD\u653E\u5728\u8FD9\u4E9B\u76EE\u5F55\u4E4B\u4E0B\uFF09\uFF1A",
148614
+ roots || "\uFF08\u65E0\uFF09",
148615
+ "",
148647
148616
  "\u5019\u9009\u76EE\u5F55\uFF1A",
148648
- list || "\uFF08\u65E0\uFF09",
148617
+ dirs || "\uFF08\u65E0\uFF09",
148618
+ "",
148619
+ "\u5019\u9009\u6A21\u578B\uFF1A",
148620
+ models || "\uFF08\u65E0\uFF09",
148649
148621
  "",
148650
148622
  "\u7528\u6237\u6D88\u606F\uFF1A",
148651
- text.slice(0, 2e3)
148623
+ input.text.slice(0, 2e3)
148652
148624
  ].join("\n");
148653
148625
  }
148626
+ function isPerm(value3) {
148627
+ return typeof value3 === "string" && QUICK_NEW_PERMS.includes(value3);
148628
+ }
148629
+ function isDirSource(value3) {
148630
+ return typeof value3 === "string" && QUICK_NEW_DIR_SOURCES.includes(value3);
148631
+ }
148654
148632
  function parseQuickNewDecision(raw) {
148655
148633
  if (!raw) return void 0;
148656
148634
  const text = raw.trim();
@@ -148665,18 +148643,27 @@ function parseQuickNewDecision(raw) {
148665
148643
  }
148666
148644
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
148667
148645
  const obj = parsed;
148668
- const intent = obj.intent === "task" ? "task" : obj.intent === "chat" ? "chat" : void 0;
148646
+ const intent = obj.intent === "create" || obj.intent === "list" || obj.intent === "chat" ? obj.intent : void 0;
148669
148647
  if (!intent) return void 0;
148670
148648
  const dir = typeof obj.dir === "string" ? obj.dir.trim() : typeof obj.directory === "string" ? obj.directory.trim() : "";
148649
+ const dirSource = isDirSource(obj.dir_source) ? obj.dir_source : isDirSource(obj.dirSource) ? obj.dirSource : void 0;
148671
148650
  const title = typeof obj.title === "string" ? obj.title.trim().slice(0, 30) : "";
148651
+ const perm = isPerm(obj.perm) ? obj.perm : void 0;
148652
+ const model = typeof obj.model === "string" ? obj.model.trim() : "";
148672
148653
  const reason = typeof obj.reason === "string" ? obj.reason.trim().slice(0, 200) : "";
148673
148654
  return {
148674
148655
  intent,
148675
148656
  ...dir ? { directory: dir } : {},
148657
+ ...dirSource ? { dirSource } : {},
148676
148658
  ...title ? { title } : {},
148659
+ ...perm ? { perm } : {},
148660
+ ...model ? { model } : {},
148677
148661
  ...reason ? { reason } : {}
148678
148662
  };
148679
148663
  }
148664
+ function slugifyTitle(title) {
148665
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/g, "");
148666
+ }
148680
148667
  function matchCandidateDirectory(directory, candidates) {
148681
148668
  if (!directory) return void 0;
148682
148669
  const normalize2 = (p) => p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
@@ -148684,6 +148671,12 @@ function matchCandidateDirectory(directory, candidates) {
148684
148671
  const hit = candidates.find((c) => normalize2(c.path) === target);
148685
148672
  return hit?.path;
148686
148673
  }
148674
+ function matchModelOption(model, models) {
148675
+ if (!model) return void 0;
148676
+ const norm = (s) => s.trim().toLowerCase();
148677
+ const target = norm(model);
148678
+ 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);
148679
+ }
148687
148680
  var SYSTEM_DIRS = [
148688
148681
  "/etc",
148689
148682
  "/usr",
@@ -148707,7 +148700,7 @@ function isUnder(child, root) {
148707
148700
  function isSystemPath(path2) {
148708
148701
  return SYSTEM_DIRS.some((dir) => isUnder(path2, dir));
148709
148702
  }
148710
- function validateDirectory(rawPath, allowedRoots, deps = {}) {
148703
+ function validateDirectory(rawPath, allowedRoots, deps = {}, opts = {}) {
148711
148704
  const raw = rawPath.trim();
148712
148705
  const roots = allowedRoots.map((r) => resolve$1(r)).filter((r) => r && r !== "");
148713
148706
  if (roots.length === 0) {
@@ -148735,10 +148728,12 @@ function validateDirectory(rawPath, allowedRoots, deps = {}) {
148735
148728
  exists2 = false;
148736
148729
  }
148737
148730
  if (!exists2) {
148738
- try {
148739
- mkdir2(target, { recursive: true });
148740
- } catch {
148741
- return err("create_failed", `\u76EE\u5F55\u4E0D\u5B58\u5728\u4E14\u65E0\u6CD5\u521B\u5EFA\uFF1A\`${target}\`\u3002\u8BF7\u68C0\u67E5\u8DEF\u5F84\u4E0E\u6743\u9650\u3002`);
148731
+ if (opts.create ?? true) {
148732
+ try {
148733
+ mkdir2(target, { recursive: true });
148734
+ } catch {
148735
+ return err("create_failed", `\u76EE\u5F55\u4E0D\u5B58\u5728\u4E14\u65E0\u6CD5\u521B\u5EFA\uFF1A\`${target}\`\u3002\u8BF7\u68C0\u67E5\u8DEF\u5F84\u4E0E\u6743\u9650\u3002`);
148736
+ }
148742
148737
  }
148743
148738
  }
148744
148739
  let real = target;
@@ -148765,6 +148760,33 @@ function forbiddenReason(path2) {
148765
148760
  function err(reason, message) {
148766
148761
  return { ok: false, reason, message };
148767
148762
  }
148763
+ var MAX_ROOT_SUBDIRS = 15;
148764
+ async function scanRootSubdirs(root, deps = {}) {
148765
+ const rootPath = root.trim();
148766
+ if (!rootPath) return [];
148767
+ const rd = deps.readdir ?? ((path2, options2) => readdir(path2, options2));
148768
+ let entries3;
148769
+ try {
148770
+ entries3 = await rd(rootPath, { withFileTypes: true });
148771
+ } catch {
148772
+ return [];
148773
+ }
148774
+ const names = entries3.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).slice(0, Math.max(1, deps.limit ?? MAX_ROOT_SUBDIRS));
148775
+ const out = [];
148776
+ for (const name of names) {
148777
+ const path2 = join(rootPath, name);
148778
+ out.push({ name, path: path2, isRepo: await hasGit(path2, rd) });
148779
+ }
148780
+ return out;
148781
+ }
148782
+ async function hasGit(path2, rd) {
148783
+ try {
148784
+ const entries3 = await rd(path2, { withFileTypes: true });
148785
+ return entries3.some((e) => e.name === ".git");
148786
+ } catch {
148787
+ return false;
148788
+ }
148789
+ }
148768
148790
 
148769
148791
  // src/feishu/perm-presets.ts
148770
148792
  var SHELL_ACTIONS = ["shell", "bash"];
@@ -149435,7 +149457,7 @@ var SETUP_FORM_FIELDS = {
149435
149457
  var SETUP_FORM_MAX_MODELS = 15;
149436
149458
  var SETUP_FORM_DEFAULT_PERM = "edit";
149437
149459
  var SETUP_FORM_DIR_CUSTOM = "__custom__";
149438
- function button4(text, type, value3) {
149460
+ function button3(text, type, value3) {
149439
149461
  return {
149440
149462
  tag: "button",
149441
149463
  text: { tag: "plain_text", content: text },
@@ -149478,28 +149500,28 @@ function buildModelCard(input) {
149478
149500
  if (input.page <= 0 && recent.length > 0) {
149479
149501
  for (const model of recent) {
149480
149502
  const isCurrent = input.current && refKey(input.current) === refKey(model);
149481
- elements.push(button4(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, isCurrent ? "primary" : "default", modelValue(model, input.sid)));
149503
+ elements.push(button3(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, isCurrent ? "primary" : "default", modelValue(model, input.sid)));
149482
149504
  }
149483
149505
  if (input.models.length > recent.length) {
149484
- elements.push(button4("\u66F4\u591A \u25B8", "default", moreValue(1, input.sid)));
149506
+ elements.push(button3("\u66F4\u591A \u25B8", "default", moreValue(1, input.sid)));
149485
149507
  }
149486
149508
  } else {
149487
149509
  const pageNum = input.page <= 0 ? 1 : input.page;
149488
149510
  const slice = input.models.slice((pageNum - 1) * pageSize, pageNum * pageSize);
149489
149511
  if (slice.length === 0 && recent.length > 0) {
149490
149512
  for (const model of recent) {
149491
- elements.push(button4(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, "default", modelValue(model, input.sid)));
149513
+ elements.push(button3(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, "default", modelValue(model, input.sid)));
149492
149514
  }
149493
149515
  } else {
149494
149516
  for (const model of slice) {
149495
149517
  const isCurrent = input.current && refKey(input.current) === refKey(model);
149496
149518
  elements.push(
149497
- button4(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, isCurrent ? "primary" : "default", modelValue(model, input.sid))
149519
+ button3(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, isCurrent ? "primary" : "default", modelValue(model, input.sid))
149498
149520
  );
149499
149521
  }
149500
- if (pageNum > 1) elements.push(button4("\u25C2 \u4E0A\u4E00\u9875", "default", moreValue(pageNum - 1, input.sid)));
149522
+ if (pageNum > 1) elements.push(button3("\u25C2 \u4E0A\u4E00\u9875", "default", moreValue(pageNum - 1, input.sid)));
149501
149523
  if (pageNum * pageSize < input.models.length) {
149502
- elements.push(button4("\u4E0B\u4E00\u9875 \u25B8", "default", moreValue(pageNum + 1, input.sid)));
149524
+ elements.push(button3("\u4E0B\u4E00\u9875 \u25B8", "default", moreValue(pageNum + 1, input.sid)));
149503
149525
  }
149504
149526
  }
149505
149527
  }
@@ -149513,7 +149535,7 @@ function buildPermCard(input) {
149513
149535
  }
149514
149536
  const elements = [{ tag: "markdown", content: truncateCardContent(lines.join("\n")) }];
149515
149537
  for (const info of PERMISSION_PRESETS) {
149516
- elements.push(button4(presetLabel(info.id), input.current === info.id ? "primary" : "default", permValue(info.id, input.sid)));
149538
+ elements.push(button3(presetLabel(info.id), input.current === info.id ? "primary" : "default", permValue(info.id, input.sid)));
149517
149539
  }
149518
149540
  return headerCard("\u{1F510} \u9009\u62E9\u6743\u9650\u6863\u4F4D", "orange", elements);
149519
149541
  }
@@ -149529,8 +149551,8 @@ function buildConfirmCard(input) {
149529
149551
  ];
149530
149552
  return headerCard("\u2705 \u786E\u8BA4\u521B\u5EFA\u4F1A\u8BDD", "green", [
149531
149553
  { tag: "markdown", content: truncateCardContent(lines.join("\n")) },
149532
- button4("\u2705 \u521B\u5EFA", "primary", { wizard: "confirm" }),
149533
- button4("\u2716\uFE0F \u53D6\u6D88", "default", { wizard: "cancel" })
149554
+ button3("\u2705 \u521B\u5EFA", "primary", { wizard: "confirm" }),
149555
+ button3("\u2716\uFE0F \u53D6\u6D88", "default", { wizard: "cancel" })
149534
149556
  ]);
149535
149557
  }
149536
149558
  function buildSetupDoneCard(title, lines, template2 = "grey") {
@@ -149598,6 +149620,9 @@ function buildSetupFormCard(input) {
149598
149620
  if (input.error) {
149599
149621
  formElements.push({ tag: "markdown", content: truncateCardContent(`\u26A0\uFE0F **\u63D0\u4EA4\u5931\u8D25**\uFF1A${input.error}`) });
149600
149622
  }
149623
+ if (input.notice) {
149624
+ formElements.push({ tag: "markdown", content: truncateCardContent(input.notice) });
149625
+ }
149601
149626
  formElements.push(
149602
149627
  { tag: "markdown", content: truncateCardContent(lines.join("\n")) },
149603
149628
  {
@@ -149830,33 +149855,6 @@ function createSessionContext(deps) {
149830
149855
  };
149831
149856
  return ctx;
149832
149857
  }
149833
- var MAX_ROOT_SUBDIRS = 15;
149834
- async function scanRootSubdirs(root, deps = {}) {
149835
- const rootPath = root.trim();
149836
- if (!rootPath) return [];
149837
- const rd = deps.readdir ?? ((path2, options2) => readdir(path2, options2));
149838
- let entries3;
149839
- try {
149840
- entries3 = await rd(rootPath, { withFileTypes: true });
149841
- } catch {
149842
- return [];
149843
- }
149844
- const names = entries3.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).slice(0, MAX_ROOT_SUBDIRS);
149845
- const out = [];
149846
- for (const name of names) {
149847
- const path2 = join(rootPath, name);
149848
- out.push({ name, path: path2, isRepo: await hasGit(path2, rd) });
149849
- }
149850
- return out;
149851
- }
149852
- async function hasGit(path2, rd) {
149853
- try {
149854
- const entries3 = await rd(path2, { withFileTypes: true });
149855
- return entries3.some((e) => e.name === ".git");
149856
- } catch {
149857
- return false;
149858
- }
149859
- }
149860
149858
 
149861
149859
  // src/session/model-perm.ts
149862
149860
  async function currentModel(ctx, sessionID) {
@@ -150038,6 +150036,21 @@ async function openSetupForm(ctx, message, title) {
150038
150036
  const res = await ctx.deps.sender.sendCard(message.chatId, card);
150039
150037
  if (!res.ok) ctx.deps.log.warn("\u8868\u5355\u5361\u53D1\u9001\u5931\u8D25", { chatId: message.chatId, error: res.error ?? "unknown" });
150040
150038
  }
150039
+ async function buildPrefilledSetupForm(ctx, chatId, anchorMessageId, prefill) {
150040
+ let state = await ctx.deps.wizard.get(chatId);
150041
+ if (!state) state = await ctx.deps.wizard.start(chatId, prefill.title, anchorMessageId);
150042
+ const next = {
150043
+ ...state,
150044
+ ...prefill.title !== void 0 ? { title: prefill.title } : {},
150045
+ ...prefill.dir !== void 0 ? { dir: prefill.dir } : {},
150046
+ ...prefill.model !== void 0 ? { model: prefill.model } : {},
150047
+ ...prefill.perm !== void 0 ? { perm: prefill.perm } : {}
150048
+ };
150049
+ await ctx.deps.wizard.set(chatId, next);
150050
+ return renderFormCard(ctx, next, {
150051
+ ...prefill.notice ? { notice: prefill.notice } : {}
150052
+ });
150053
+ }
150041
150054
  async function cmdDir(ctx, message, args2) {
150042
150055
  if (message.threadId) {
150043
150056
  await ctx.reply(message, threadForbiddenText("dir"));
@@ -150401,6 +150414,7 @@ async function renderFormCard(ctx, state, over) {
150401
150414
  ...rootSubdirs.length > 0 ? { rootSubdirs } : {},
150402
150415
  ...ctx.deps.allowedRoots ? { allowedRoots: ctx.deps.allowedRoots } : {},
150403
150416
  ...over?.error ? { error: over.error } : {},
150417
+ ...over?.notice ? { notice: over.notice } : {},
150404
150418
  values: values2
150405
150419
  });
150406
150420
  }
@@ -150432,7 +150446,8 @@ function createSetupWizardApi(ctx) {
150432
150446
  cmdCancel: (message) => cmdCancel(ctx, message),
150433
150447
  applySetupCardAction: (action, value3) => applySetupCardAction(ctx, action, value3),
150434
150448
  applySetupFormSubmit: (action) => applySetupFormSubmit(ctx, action),
150435
- sendSetupFormForChat: (chatId, anchorMessageId, openId) => sendSetupFormForChat(ctx, chatId, anchorMessageId, openId)
150449
+ sendSetupFormForChat: (chatId, anchorMessageId, openId) => sendSetupFormForChat(ctx, chatId, anchorMessageId, openId),
150450
+ buildPrefilledSetupForm: (chatId, anchorMessageId, prefill) => buildPrefilledSetupForm(ctx, chatId, anchorMessageId, prefill)
150436
150451
  };
150437
150452
  }
150438
150453
 
@@ -150639,10 +150654,17 @@ async function patchResumeSummary(ctx, sessionID, dir, messageId, cardInput, per
150639
150654
  ...dir ? { directory: dir } : {},
150640
150655
  timeoutMs: ctx.deps.resumeSummaryTimeoutMs ?? 15e3
150641
150656
  });
150657
+ const base2 = await ctx.deps.sessionMap.getRootCard(sessionID);
150658
+ if (base2?.compactPending || base2?.summaryLabel === "\u5DF2\u538B\u7F29 \xB7 \u4F1A\u8BDD\u6458\u8981") {
150659
+ if (base2.summaryPending) {
150660
+ await ctx.deps.sessionMap.setRootCard(sessionID, { ...base2, summaryPending: false });
150661
+ }
150662
+ ctx.deps.log.info("\u6062\u590D\u5361\u6458\u8981\u8BA9\u4F4D\u538B\u7F29\u6D41\u7A0B\uFF08\u8DF3\u8FC7 patch\uFF09", { sessionID, source: outcome.source });
150663
+ return;
150664
+ }
150642
150665
  const summary = outcome.summary ?? "\uFF08\u6458\u8981\u751F\u6210\u5931\u8D25\uFF0C\u53EF\u76F4\u63A5\u53D1\u6D88\u606F\u7EE7\u7EED\uFF09";
150643
150666
  const summaryLabel = SUMMARY_LABEL[outcome.source] ?? "\u6458\u8981";
150644
150667
  if (persistBase) {
150645
- const base2 = await ctx.deps.sessionMap.getRootCard(sessionID);
150646
150668
  if (base2) {
150647
150669
  await ctx.deps.sessionMap.setRootCard(sessionID, { ...base2, summaryPending: false, summary, summaryLabel });
150648
150670
  }
@@ -150936,6 +150958,19 @@ var SessionCommands = class {
150936
150958
  );
150937
150959
  this.ctx = ctx;
150938
150960
  }
150961
+ /** AI 管理台:构建会话列表卡(不发送;调用方自行 patch / 发送)。 */
150962
+ async buildSessionListCard(chatId, page = 0) {
150963
+ const entries3 = await this.ctx.loadSessionEntries(chatId);
150964
+ const active = await this.ctx.deps.sessionMap.getActive(chatId);
150965
+ return this.ctx.buildListCard(chatId, entries3, page, active?.sessionID);
150966
+ }
150967
+ /**
150968
+ * AI 管理台:构建 AI 预填的建会话表单卡(写入向导状态,**不发送**)。
150969
+ * 表单消息 id 即后续话题锚点;用户确认/修改后提交走既有表单流程。
150970
+ */
150971
+ async buildPrefilledSetupForm(chatId, anchorMessageId, prefill) {
150972
+ return this.ctx.buildPrefilledSetupForm(chatId, anchorMessageId, prefill);
150973
+ }
150939
150974
  /**
150940
150975
  * 文本命令拦截。返回 true 表示已作为命令处理(调用方**不得**再发 prompt)。
150941
150976
  * 非命令返回 false。
@@ -151520,8 +151555,7 @@ function routeCardAction(action, deps) {
151520
151555
  return deps.handleCompact(action);
151521
151556
  }
151522
151557
  const hasForm = action.formValue !== void 0;
151523
- const routed = hasForm || isSetupFormAction(value3) || Boolean(parseSessionCardValue(value3)) || Boolean(parseSetupCardValue(value3)) || // 「一句话建会话」建议卡按钮(issue #2)。
151524
- Boolean(parseQuickNewActionValue(value3));
151558
+ const routed = hasForm || isSetupFormAction(value3) || Boolean(parseSessionCardValue(value3)) || Boolean(parseSetupCardValue(value3));
151525
151559
  deps.log.debug("\u5361\u7247\u56DE\u8C03\u8DEF\u7531", {
151526
151560
  hasForm,
151527
151561
  hasValue: value3 !== void 0,
@@ -151697,6 +151731,7 @@ async function compactSessionHttp(sessionID, directory, deps) {
151697
151731
  }
151698
151732
 
151699
151733
  // src/session/quick-generate.ts
151734
+ var TEMP_SESSION_TITLE = "\u2699\uFE0F \u5185\u90E8\u751F\u6210\uFF08\u4E34\u65F6\uFF0C\u53EF\u5FFD\u7565\uFF09";
151700
151735
  function sessionRoutingHeaders(sessionID, directory) {
151701
151736
  const headers = { "x-opencode-session": sessionID };
151702
151737
  if (directory) headers["x-opencode-directory"] = encodeURIComponent(directory);
@@ -151707,12 +151742,38 @@ function authHeaders3(service3) {
151707
151742
  const token = Buffer.from(`opencode:${service3.password}`, "utf8").toString("base64");
151708
151743
  return { authorization: `Basic ${token}` };
151709
151744
  }
151745
+ function sessionChannelEnabled(value3) {
151746
+ if (value3 === "on") return true;
151747
+ if (value3 === "off") return false;
151748
+ const env = process.env;
151749
+ return env["VITEST"] === void 0 && env["NODE_ENV"] !== "test";
151750
+ }
151751
+ function httpChannelAllowed(deps) {
151752
+ if (deps.discover || deps.fetchImpl) return true;
151753
+ const env = process.env;
151754
+ return env["VITEST"] === void 0 && env["NODE_ENV"] !== "test";
151755
+ }
151710
151756
  async function quickGenerateWithSession(deps, input) {
151757
+ const httpAllowed = httpChannelAllowed(deps);
151758
+ const discover = deps.discover ?? (() => discoverLocalService());
151759
+ const service3 = httpAllowed ? await discover().catch(() => void 0) : void 0;
151760
+ if (service3 && sessionChannelEnabled(deps.sessionChannel)) {
151761
+ try {
151762
+ const result4 = await generateViaTemporarySession(deps, input, service3);
151763
+ deps.log.info("\u5FEB\u6458\u8981\u751F\u6210\u8D70\u4E34\u65F6\u4F1A\u8BDD", { sessionID: input.sessionID, route: "session" });
151764
+ return { route: "session", result: result4 };
151765
+ } catch (err2) {
151766
+ deps.log.debug("\u4E34\u65F6\u4F1A\u8BDD\u751F\u6210\u5931\u8D25\uFF0C\u56DE\u9000\u751F\u6210 API / \u672C\u673A HTTP", {
151767
+ sessionID: input.sessionID,
151768
+ error: errorMessage(err2)
151769
+ });
151770
+ }
151771
+ }
151711
151772
  const headers = sessionRoutingHeaders(input.sessionID, input.directory);
151712
151773
  if (deps.generateText) {
151713
151774
  try {
151714
151775
  const result4 = await withAttemptTimeout(
151715
- deps.generateText(input.prompt, { headers }),
151776
+ deps.generateText(input.prompt, { headers }, input.model),
151716
151777
  deps.attemptTimeoutMs ?? 8e3
151717
151778
  );
151718
151779
  if (result4 !== void 0 && result4 !== null) {
@@ -151727,14 +151788,20 @@ async function quickGenerateWithSession(deps, input) {
151727
151788
  });
151728
151789
  }
151729
151790
  }
151730
- const discover = deps.discover ?? (() => discoverLocalService());
151731
151791
  const doFetch = deps.fetchImpl ?? fetch;
151732
- const service3 = await discover();
151733
- 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");
151792
+ if (!service3) {
151793
+ throw new Error(
151794
+ 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"
151795
+ );
151796
+ }
151734
151797
  const response2 = await doFetch(`${service3.url}/api/experimental/generate`, {
151735
151798
  method: "POST",
151736
151799
  headers: { "content-type": "application/json", ...authHeaders3(service3), ...headers },
151737
- body: JSON.stringify({ prompt: input.prompt }),
151800
+ body: JSON.stringify({
151801
+ prompt: input.prompt,
151802
+ // 显式模型:未指定时服务端依赖"受支持的默认模型",实际环境常见 400。
151803
+ ...input.model ? { model: input.model } : {}
151804
+ }),
151738
151805
  signal: AbortSignal.timeout(deps.timeoutMs ?? 15e3)
151739
151806
  });
151740
151807
  if (!response2.ok) {
@@ -151748,6 +151815,56 @@ async function quickGenerateWithSession(deps, input) {
151748
151815
  deps.log.info("\u5FEB\u6458\u8981\u751F\u6210\u8D70\u672C\u673A HTTP \u751F\u6210", { sessionID: input.sessionID, route: "http" });
151749
151816
  return { route: "http", result: result3 };
151750
151817
  }
151818
+ async function generateViaTemporarySession(deps, input, service3) {
151819
+ const doFetch = deps.fetchImpl ?? fetch;
151820
+ const base2 = service3.url.replace(/\/+$/, "");
151821
+ const auth = authHeaders3(service3);
151822
+ const dirHeader = input.directory ? { "x-opencode-directory": encodeURIComponent(input.directory) } : {};
151823
+ const timeout3 = deps.timeoutMs ?? 15e3;
151824
+ const created = await doFetch(`${base2}/api/session`, {
151825
+ method: "POST",
151826
+ headers: { "content-type": "application/json", ...auth, ...dirHeader },
151827
+ body: JSON.stringify({
151828
+ title: TEMP_SESSION_TITLE,
151829
+ ...input.model ? { model: input.model } : {},
151830
+ ...input.directory ? { location: { directory: input.directory } } : {}
151831
+ }),
151832
+ signal: AbortSignal.timeout(timeout3)
151833
+ });
151834
+ if (!created.ok) {
151835
+ const detail = await created.text().catch(() => "");
151836
+ throw new Error(`\u4E34\u65F6\u4F1A\u8BDD\u521B\u5EFA\u5931\u8D25 HTTP ${created.status}${detail ? `: ${detail.slice(0, 160)}` : ""}`);
151837
+ }
151838
+ const tempID = extractSessionID(await created.json());
151839
+ if (!tempID) throw new Error("\u4E34\u65F6\u4F1A\u8BDD\u521B\u5EFA\u54CD\u5E94\u7F3A\u5C11\u4F1A\u8BDD id");
151840
+ try {
151841
+ const generated = await doFetch(`${base2}/api/session/${encodeURIComponent(tempID)}/generate`, {
151842
+ method: "POST",
151843
+ headers: { "content-type": "application/json", ...auth, ...dirHeader },
151844
+ body: JSON.stringify({ prompt: input.prompt }),
151845
+ signal: AbortSignal.timeout(timeout3)
151846
+ });
151847
+ if (!generated.ok) {
151848
+ const detail = await generated.text().catch(() => "");
151849
+ throw new Error(`\u4E34\u65F6\u4F1A\u8BDD\u751F\u6210\u5931\u8D25 HTTP ${generated.status}${detail ? `: ${detail.slice(0, 160)}` : ""}`);
151850
+ }
151851
+ return await generated.json();
151852
+ } finally {
151853
+ await doFetch(`${base2}/api/session/${encodeURIComponent(tempID)}`, {
151854
+ method: "DELETE",
151855
+ headers: { ...auth, ...dirHeader },
151856
+ signal: AbortSignal.timeout(5e3)
151857
+ }).catch(() => void 0);
151858
+ }
151859
+ }
151860
+ function extractSessionID(raw) {
151861
+ if (typeof raw !== "object" || raw === null) return void 0;
151862
+ const record2 = raw;
151863
+ const data = record2["data"];
151864
+ const fromData = typeof data === "object" && data !== null ? data["id"] : void 0;
151865
+ const id2 = typeof record2["id"] === "string" ? record2["id"] : fromData;
151866
+ return typeof id2 === "string" && id2.startsWith("ses") ? id2 : void 0;
151867
+ }
151751
151868
  function withAttemptTimeout(promise3, ms) {
151752
151869
  return new Promise((resolve4, reject) => {
151753
151870
  const timer = setTimeout(() => reject(new Error("generate-attempt-timeout")), Math.max(1, ms));
@@ -152010,6 +152127,12 @@ async function start(ctx, config, log2, logSink) {
152010
152127
  return void 0;
152011
152128
  }
152012
152129
  }
152130
+ async function resolveGenerateModel(sessionID, directory) {
152131
+ const sessionModel = await readSessionModelQuiet(sessionID, directory);
152132
+ if (sessionModel) return { providerID: sessionModel.providerID, id: sessionModel.id };
152133
+ const first = (await listModels())[0];
152134
+ return first ? { providerID: first.providerID, id: first.id } : void 0;
152135
+ }
152013
152136
  async function switchSessionModel(sessionID, model) {
152014
152137
  const link2 = await sessionMap.resolveBySession(sessionID);
152015
152138
  const options2 = link2?.dir ? { headers: { "x-opencode-directory": link2.dir } } : void 0;
@@ -152112,14 +152235,20 @@ async function start(ctx, config, log2, logSink) {
152112
152235
  const readMessages = async (sessionID, directory) => readSessionMessages(ctx, sessionID, directory, log2);
152113
152236
  const generateText = async (prompt, directory) => {
152114
152237
  const api = ctx.generate.text;
152238
+ const model = await resolveGenerateModel(input.sessionID, directory);
152115
152239
  const outcome = await quickGenerateWithSession(
152116
152240
  {
152117
152241
  log: log2,
152118
152242
  ...typeof api === "function" ? {
152119
- generateText: (p, requestOptions) => api({ prompt: p }, requestOptions)
152243
+ generateText: (p, requestOptions, modelRef) => api({ prompt: p, ...modelRef ? { model: modelRef } : {} }, requestOptions)
152120
152244
  } : {}
152121
152245
  },
152122
- { prompt, sessionID: input.sessionID, ...directory ? { directory } : {} }
152246
+ {
152247
+ prompt,
152248
+ sessionID: input.sessionID,
152249
+ ...directory ? { directory } : {},
152250
+ ...model ? { model } : {}
152251
+ }
152123
152252
  );
152124
152253
  return outcome.result;
152125
152254
  };
@@ -152382,26 +152511,41 @@ async function start(ctx, config, log2, logSink) {
152382
152511
  log2.info("\u673A\u5668\u4EBA\u83DC\u5355\u70B9\u51FB \u2192 \u6267\u884C\u547D\u4EE4", { eventKey: click.eventKey, cmd: entry.cmd, label: entry.label });
152383
152512
  await handleMessage(synthetic);
152384
152513
  }
152385
- const QUICK_NEW_KEY_PREFIX = "feishu:v2:quicknew:";
152386
- const QUICK_NEW_TTL_MS = 60 * 60 * 1e3;
152387
- function parseQuickNewEntry(raw) {
152388
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
152389
- const v = raw;
152390
- const text = typeof v.text === "string" ? v.text : "";
152391
- const dir = typeof v.dir === "string" ? v.dir : "";
152392
- const chatId = typeof v.chatId === "string" ? v.chatId : "";
152393
- const openId = typeof v.openId === "string" ? v.openId : "";
152394
- const anchorMessageId = typeof v.anchorMessageId === "string" ? v.anchorMessageId : "";
152395
- if (!text || !dir || !chatId || !openId || !anchorMessageId) return void 0;
152396
- return {
152397
- text,
152398
- title: typeof v.title === "string" ? v.title : "",
152399
- dir,
152400
- chatId,
152401
- openId,
152402
- anchorMessageId,
152403
- createdAt: typeof v.createdAt === "number" ? v.createdAt : 0
152514
+ function resolveConsoleDir(decision, candidates) {
152515
+ const dry = (path2) => validateDirectory(path2, config.allowedRoots, {}, { create: false });
152516
+ const root = config.allowedRoots[0];
152517
+ const newNotice = "\u2795 **AI \u65B0\u5EFA\u76EE\u5F55**\uFF08\u4E0D\u5B58\u5728\u65F6\u4F1A\u5728\u521B\u5EFA\u65F6\u81EA\u52A8\u521B\u5EFA\uFF1B\u53EF\u5728\u4E0B\u65B9\u4FEE\u6539\uFF09";
152518
+ const fallbackNew = () => {
152519
+ if (!root) return { notice: "\u26A0\uFE0F \u672A\u914D\u7F6E\u5141\u8BB8\u7684\u6839\u76EE\u5F55\uFF0C\u8BF7\u5728\u4E0B\u65B9\u586B\u5199\u76EE\u5F55\u3002", source: "invalid" };
152520
+ const slug = slugifyTitle(decision.title ?? "");
152521
+ const candidate = slug ? `${root.replace(/\/+$/, "")}/${slug}` : root;
152522
+ const check2 = dry(candidate);
152523
+ if (check2.ok) {
152524
+ return {
152525
+ dir: check2.path,
152526
+ notice: slug ? newNotice : "\u{1F3E0} \u4F7F\u7528**\u5141\u8BB8\u6839\u76EE\u5F55**\uFF08\u53EF\u5728\u4E0B\u65B9\u4FEE\u6539\uFF09",
152527
+ source: "new"
152528
+ };
152529
+ }
152530
+ return { dir: root, notice: "\u{1F3E0} \u4F7F\u7528**\u5141\u8BB8\u6839\u76EE\u5F55**\uFF08\u53EF\u5728\u4E0B\u65B9\u4FEE\u6539\uFF09", source: "new" };
152404
152531
  };
152532
+ if (decision.dirSource === "given" && decision.directory) {
152533
+ const check2 = dry(decision.directory);
152534
+ return check2.ok ? { dir: check2.path, notice: "\u270D\uFE0F \u76EE\u5F55\u7531**\u4F60\u6307\u5B9A**\uFF08\u53EF\u5728\u4E0B\u65B9\u4FEE\u6539\uFF09", source: "given" } : {
152535
+ notice: `\u26A0\uFE0F \u4F60\u6307\u5B9A\u7684\u76EE\u5F55 \`${decision.directory}\` \u4E0D\u53EF\u7528\uFF1A${check2.message}`,
152536
+ source: "invalid"
152537
+ };
152538
+ }
152539
+ const matched = matchCandidateDirectory(decision.directory, candidates);
152540
+ if (matched) {
152541
+ return { dir: matched, notice: "\u2713 \u5DF2\u5339\u914D**\u5386\u53F2 / \u6700\u8FD1\u76EE\u5F55**\uFF08\u53EF\u5728\u4E0B\u65B9\u4FEE\u6539\uFF09", source: "existing" };
152542
+ }
152543
+ const proposed = decision.directory?.trim();
152544
+ if (proposed) {
152545
+ const check2 = dry(proposed);
152546
+ if (check2.ok) return { dir: check2.path, notice: newNotice, source: "new" };
152547
+ }
152548
+ return fallbackNew();
152405
152549
  }
152406
152550
  async function handleQuickNew(message) {
152407
152551
  const hint = async () => {
@@ -152411,12 +152555,12 @@ async function start(ctx, config, log2, logSink) {
152411
152555
  try {
152412
152556
  const candidates = [];
152413
152557
  const seen = /* @__PURE__ */ new Set();
152414
- for (const dir2 of await recent.listDirs()) {
152415
- if (!seen.has(dir2)) {
152416
- seen.add(dir2);
152417
- candidates.push({ path: dir2 });
152418
- }
152419
- }
152558
+ const pushCandidate = (path2, label) => {
152559
+ const trimmed = path2.trim();
152560
+ if (!trimmed || seen.has(trimmed)) return;
152561
+ seen.add(trimmed);
152562
+ candidates.push({ path: trimmed, ...label ? { label } : {} });
152563
+ };
152420
152564
  let entries3 = [];
152421
152565
  try {
152422
152566
  const raw = await listAllSessionsRaw() ?? await listSessionsOverHttp({}, { log: log2 });
@@ -152424,200 +152568,111 @@ async function start(ctx, config, log2, logSink) {
152424
152568
  } catch (err2) {
152425
152569
  log2.debug("quick-new \u4F1A\u8BDD\u5217\u8868\u8BFB\u53D6\u5931\u8D25", { error: errorMessage(err2) });
152426
152570
  }
152427
- for (const entry2 of entries3) {
152428
- if (entry2.directory && !seen.has(entry2.directory)) {
152429
- seen.add(entry2.directory);
152430
- candidates.push({ path: entry2.directory, ...entry2.title ? { label: entry2.title } : {} });
152571
+ const sessionLabelByDir = /* @__PURE__ */ new Map();
152572
+ for (const entry of entries3) {
152573
+ if (entry.directory && entry.title) sessionLabelByDir.set(entry.directory, entry.title);
152574
+ }
152575
+ for (const dir of await recent.listDirs()) pushCandidate(dir);
152576
+ for (const root of config.allowedRoots.slice(0, 3)) {
152577
+ try {
152578
+ const subdirs = await scanRootSubdirs(root, { limit: 50 });
152579
+ for (const sub of subdirs) {
152580
+ pushCandidate(sub.path, sessionLabelByDir.get(sub.path) ?? (sub.isRepo ? "git \u4ED3\u5E93" : void 0));
152581
+ }
152582
+ } catch (err2) {
152583
+ log2.debug("quick-new \u4E00\u7EA7\u76EE\u5F55\u626B\u63CF\u5931\u8D25", { root, error: errorMessage(err2) });
152431
152584
  }
152432
152585
  }
152586
+ for (const entry of entries3) {
152587
+ if (entry.directory) pushCandidate(entry.directory, entry.title);
152588
+ }
152433
152589
  const routingSessionID = entries3[0]?.sessionID;
152434
- if (candidates.length === 0 || !routingSessionID) {
152435
- log2.debug("quick-new \u65E0\u5019\u9009\u76EE\u5F55\u6216\u4F1A\u8BDD\uFF0C\u56DE\u9000\u63D0\u793A\u5361", {
152436
- candidates: candidates.length,
152437
- hasRouting: Boolean(routingSessionID)
152438
- });
152590
+ if (!routingSessionID) {
152591
+ 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");
152439
152592
  await hint();
152440
152593
  return;
152441
152594
  }
152595
+ let models = [];
152596
+ try {
152597
+ models = (await listModels()).map((m) => ({
152598
+ providerID: m.providerID,
152599
+ id: m.id,
152600
+ ...m.name ? { name: m.name } : {}
152601
+ }));
152602
+ } catch (err2) {
152603
+ log2.debug("quick-new \u6A21\u578B\u5217\u8868\u8BFB\u53D6\u5931\u8D25", { error: errorMessage(err2) });
152604
+ }
152442
152605
  const ack = await sender.sendCard(message.chatId, buildQuickNewThinkingCard());
152443
152606
  if (!ack.ok || !ack.messageId) {
152444
152607
  await hint();
152445
152608
  return;
152446
152609
  }
152447
- const generateText = async (prompt, requestOptions) => {
152610
+ const genModel = await resolveGenerateModel(routingSessionID, entries3[0]?.directory);
152611
+ const generateText = async (prompt, requestOptions, model2) => {
152448
152612
  const api = ctx.generate.text;
152449
152613
  if (typeof api !== "function") throw new Error("generate.text unavailable");
152450
- return api({ prompt }, requestOptions);
152614
+ return api({ prompt, ...model2 ? { model: model2 } : {} }, requestOptions);
152451
152615
  };
152452
152616
  const outcome = await quickGenerateWithSession(
152453
152617
  { log: log2, generateText },
152454
- { prompt: buildQuickNewPrompt(message.text, candidates), sessionID: routingSessionID }
152618
+ {
152619
+ prompt: buildQuickNewPrompt({
152620
+ text: message.text,
152621
+ candidates,
152622
+ models,
152623
+ allowedRoots: config.allowedRoots
152624
+ }),
152625
+ sessionID: routingSessionID,
152626
+ ...genModel ? { model: genModel } : {}
152627
+ }
152455
152628
  );
152456
152629
  const decision = parseQuickNewDecision(extractGeneratedText(outcome.result));
152457
- const dir = decision?.intent === "task" ? matchCandidateDirectory(decision.directory, candidates) : void 0;
152458
- const validation = dir ? validateDir(dir) : void 0;
152459
- if (!decision || decision.intent !== "task" || !dir || !validation?.ok) {
152460
- log2.debug("quick-new \u5224\u5B9A\u4E3A\u975E\u4EFB\u52A1\u6216\u672A\u5339\u914D\u5230\u76EE\u5F55", {
152461
- intent: decision?.intent,
152462
- dir: decision?.directory,
152463
- reason: decision?.reason
152464
- });
152630
+ log2.debug("quick-new \u8BC6\u522B\u7ED3\u679C", {
152631
+ intent: decision?.intent,
152632
+ dir: decision?.directory,
152633
+ perm: decision?.perm,
152634
+ model: decision?.model,
152635
+ reason: decision?.reason
152636
+ });
152637
+ if (!decision || decision.intent === "chat") {
152465
152638
  await sender.patchCard(ack.messageId, buildConsoleHintCard());
152466
152639
  return;
152467
152640
  }
152468
- const id2 = `qn_${message.messageId}`;
152469
- const title = decision.title || topicTitle(message.text);
152470
- const entry = {
152471
- text: message.text,
152472
- title,
152473
- dir: validation.path,
152474
- chatId: message.chatId,
152475
- openId: message.senderOpenId,
152476
- anchorMessageId: message.messageId,
152477
- createdAt: Date.now()
152641
+ if (decision.intent === "list") {
152642
+ const card = await commands.buildSessionListCard(message.chatId);
152643
+ await sender.patchCard(ack.messageId, card);
152644
+ log2.info("quick-new\uFF1A\u8BC6\u522B\u5361\u5DF2\u5C31\u5730\u5207\u6362\u4E3A\u4F1A\u8BDD\u5217\u8868\u5361", { chatId: message.chatId });
152645
+ return;
152646
+ }
152647
+ const resolution = resolveConsoleDir(decision, candidates);
152648
+ const model = matchModelOption(decision.model, models);
152649
+ const prefill = {
152650
+ title: decision.title || topicTitle(message.text),
152651
+ ...resolution.dir ? { dir: resolution.dir } : {},
152652
+ ...model ? {
152653
+ model: {
152654
+ providerID: model.providerID,
152655
+ id: model.id,
152656
+ ...model.name ? { name: model.name } : {}
152657
+ }
152658
+ } : {},
152659
+ ...decision.perm ? { perm: decision.perm } : {},
152660
+ notice: resolution.notice
152478
152661
  };
152479
- await ctx.storage.set(
152480
- QUICK_NEW_KEY_PREFIX + id2,
152481
- entry
152482
- );
152483
- await sender.patchCard(
152484
- ack.messageId,
152485
- buildQuickNewProposalCard({
152486
- id: id2,
152487
- title,
152488
- directory: validation.path,
152489
- textPreview: message.text,
152490
- ...decision.reason ? { reason: decision.reason } : {}
152491
- })
152492
- );
152493
- log2.info("quick-new \u5EFA\u8BAE\u5361\u5DF2\u53D1\u9001", { dir: validation.path, title, reason: decision.reason });
152662
+ const form = await commands.buildPrefilledSetupForm(message.chatId, ack.messageId, prefill);
152663
+ await sender.patchCard(ack.messageId, form);
152664
+ log2.info("quick-new\uFF1AAI \u9884\u586B\u5EFA\u4F1A\u8BDD\u8868\u5355\u5DF2\u53D1\u9001\uFF08\u5C31\u5730\uFF09", {
152665
+ title: prefill.title,
152666
+ dir: resolution.dir,
152667
+ dirSource: resolution.source,
152668
+ perm: decision.perm,
152669
+ model: model ? `${model.providerID}/${model.id}` : void 0
152670
+ });
152494
152671
  } catch (err2) {
152495
152672
  log2.warn("quick-new \u5904\u7406\u5931\u8D25\uFF0C\u56DE\u9000\u7BA1\u7406\u53F0\u63D0\u793A", { error: errorMessage(err2) });
152496
152673
  await hint();
152497
152674
  }
152498
152675
  }
152499
- async function handleQuickNewCardAction(action, value3) {
152500
- if (!owner.isAllowed(action.operatorOpenId)) {
152501
- return { toast: { type: "error", content: "\u65E0\u64CD\u4F5C\u6743\u9650" } };
152502
- }
152503
- const key = QUICK_NEW_KEY_PREFIX + value3.id;
152504
- let raw;
152505
- try {
152506
- raw = await ctx.storage.get(key);
152507
- } catch {
152508
- raw = void 0;
152509
- }
152510
- const entry = parseQuickNewEntry(raw);
152511
- if (!entry || Date.now() - entry.createdAt > QUICK_NEW_TTL_MS) {
152512
- try {
152513
- await ctx.storage.remove(key);
152514
- } catch {
152515
- }
152516
- if (action.messageId) {
152517
- await sender.patchCard(
152518
- action.messageId,
152519
- buildQuickNewResolvedCard("\u231B\uFE0F \u5EFA\u8BAE\u5DF2\u8FC7\u671F", "\u8BF7\u91CD\u65B0\u53D1\u9001\u6D88\u606F\u3002", "orange")
152520
- );
152521
- }
152522
- return { toast: { type: "warning", content: "\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u53D1\u6D88\u606F" } };
152523
- }
152524
- if (value3.op === "cancel") {
152525
- try {
152526
- await ctx.storage.remove(key);
152527
- } catch {
152528
- }
152529
- if (action.messageId) {
152530
- await sender.patchCard(action.messageId, buildQuickNewResolvedCard("\u274C \u5DF2\u53D6\u6D88", "\u672A\u521B\u5EFA\u4F1A\u8BDD\u3002"));
152531
- }
152532
- return { toast: { type: "info", content: "\u5DF2\u53D6\u6D88" } };
152533
- }
152534
- const validation = validateDir(entry.dir);
152535
- if (!validation.ok) {
152536
- if (action.messageId) {
152537
- await sender.patchCard(
152538
- action.messageId,
152539
- buildQuickNewResolvedCard("\u26A0\uFE0F \u76EE\u5F55\u4E0D\u53EF\u7528", `${entry.dir}\uFF1A${validation.message}`, "red")
152540
- );
152541
- }
152542
- return { toast: { type: "error", content: "\u76EE\u5F55\u4E0D\u53EF\u7528" } };
152543
- }
152544
- try {
152545
- await ctx.storage.remove(key);
152546
- } catch {
152547
- }
152548
- const perm = "edit";
152549
- const title = entry.title || topicTitle(entry.text);
152550
- void (async () => {
152551
- const created = await createSessionInternal({
152552
- title,
152553
- chatId: entry.chatId,
152554
- openId: entry.openId,
152555
- setActive: false,
152556
- directory: validation.path,
152557
- permissions: presetToRuleset(perm),
152558
- perm,
152559
- gateMode: presetGateMode(perm)
152560
- });
152561
- await recent.addDir(validation.path);
152562
- const readyCard = buildSessionReadyCard({
152563
- title,
152564
- sessionID: created.id,
152565
- dir: validation.path,
152566
- perm: presetLabel(perm)
152567
- });
152568
- const res = await sender.replyCard(entry.anchorMessageId, readyCard, { replyInThread: true });
152569
- if (res.ok && res.messageId) {
152570
- await sessionMap.bindRoot(entry.anchorMessageId, created.id);
152571
- const meta = res.threadId ? void 0 : await sender.getMessageMeta(res.messageId);
152572
- const threadId = res.threadId ?? meta?.threadId;
152573
- if (threadId) {
152574
- await sessionMap.bindThread(threadId, created.id, entry.chatId, entry.openId, entry.anchorMessageId);
152575
- } else {
152576
- log2.warn("quick-new \u5F00\u8BDD\u9898\u540E\u672A\u8BFB\u5230 thread_id", { sessionID: created.id });
152577
- }
152578
- await sessionMap.setRootCard(created.id, {
152579
- style: "created",
152580
- sessionID: created.id,
152581
- title,
152582
- dir: validation.path,
152583
- perm: presetLabel(perm)
152584
- });
152585
- } else {
152586
- log2.warn("quick-new \u5F00\u8BDD\u9898\u5931\u8D25", { sessionID: created.id, error: res.error ?? "unknown" });
152587
- }
152588
- await runInSession(
152589
- {
152590
- eventId: `quicknew_${value3.id}`,
152591
- messageId: entry.anchorMessageId,
152592
- chatId: entry.chatId,
152593
- text: entry.text,
152594
- senderOpenId: entry.openId
152595
- },
152596
- created.id,
152597
- entry.anchorMessageId
152598
- );
152599
- if (action.messageId) {
152600
- await sender.patchCard(
152601
- action.messageId,
152602
- buildQuickNewResolvedCard(
152603
- "\u2705 \u5DF2\u521B\u5EFA",
152604
- `\u76EE\u5F55\uFF1A\`${validation.path}\`
152605
- \u5DF2\u5F00\u8BDD\u9898\u5E76\u5F00\u59CB\u5904\u7406\u4F60\u7684\u6D88\u606F\u3002`,
152606
- "green"
152607
- )
152608
- );
152609
- }
152610
- })().catch((err2) => {
152611
- log2.warn("quick-new \u5EFA\u4F1A\u8BDD\u5931\u8D25", { error: errorMessage(err2) });
152612
- if (action.messageId) {
152613
- void sender.patchCard(
152614
- action.messageId,
152615
- buildQuickNewResolvedCard("\u26A0\uFE0F \u521B\u5EFA\u5931\u8D25", errorMessage(err2).slice(0, 160), "red")
152616
- );
152617
- }
152618
- });
152619
- return { toast: { type: "success", content: "\u5DF2\u521B\u5EFA\uFF0C\u6B63\u5728\u6253\u5F00\u8BDD\u9898\u2026" } };
152620
- }
152621
152676
  async function runInActiveSession(message) {
152622
152677
  let active = await sessionMap.getActive(message.chatId);
152623
152678
  if (!active) {
@@ -152634,7 +152689,7 @@ async function start(ctx, config, log2, logSink) {
152634
152689
  await runInSession(message, active.sessionID);
152635
152690
  }
152636
152691
  async function runInSession(message, sessionID, replyToMessageId, forceDelivery) {
152637
- const delivery = forceDelivery ?? decideDelivery(executions.isRunning(sessionID));
152692
+ const delivery = forceDelivery ?? decideDelivery(executions.isRunning(sessionID), config.busyDelivery);
152638
152693
  const link2 = await sessionMap.resolveBySession(sessionID);
152639
152694
  const modelRef = await readSessionModelQuiet(sessionID, link2?.dir) ?? link2?.model;
152640
152695
  const model = modelRef ? modelLabel(modelRef) : void 0;
@@ -152692,11 +152747,7 @@ ${downloadedAttachmentPrompt(message.attachment, outcome)}`;
152692
152747
  handleForm: (a) => formRelay.handleCardAction(a),
152693
152748
  handleStop: (a) => stop.handleCardAction(a),
152694
152749
  handleCompact: (a) => compact2.handleCardAction(a),
152695
- handleCommands: (a) => {
152696
- const quick = parseQuickNewActionValue(a.rawValue);
152697
- if (quick) return handleQuickNewCardAction(a, quick);
152698
- return commands.handleCardAction(a);
152699
- },
152750
+ handleCommands: (a) => commands.handleCardAction(a),
152700
152751
  handleApprovals: (a) => approvals.handleCardAction(a)
152701
152752
  })
152702
152753
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-feishu-plugin",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
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",