opencode-feishu-plugin 0.2.15 → 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
@@ -132,18 +132,24 @@ After configuring the menu (setup step 6), two quick buttons stay pinned **above
132
132
 
133
133
  Send plain text in the main chat and the AI judges the intent and handles it:
134
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:
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
136
 
137
137
  ```
138
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
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
+ [✅ 创建会话]
141
145
  ```
142
146
 
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.
148
+
143
149
  - **List sessions**: say "what sessions do I have?" → a session list card appears directly (same as `/sessions`; paginate / enter / create);
144
150
  - **Chit-chat / other**: the usual console hint card.
145
151
 
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.
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.
147
153
 
148
154
  ### Inside a topic (work)
149
155
 
@@ -229,6 +235,22 @@ Iterating from real usage feedback; current plan:
229
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).
230
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).
231
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).
253
+
232
254
  ## Advanced topics & development
233
255
 
234
256
  Security model, location keep-alive, card guard, session resume, multi-instance gateway election, full config reference, and development architecture → [docs/advanced.en.md](./docs/advanced.en.md).
package/README.md CHANGED
@@ -132,18 +132,24 @@ tail -f ~/.config/opencode/plugins/feishu.log # 应看到「飞书长连接已
132
132
 
133
133
  主聊天流发普通文本,AI 会判断意图并直接处理:
134
134
 
135
- - **建会话**:解析 目录 / 标题 / 权限档位 / 模型,把卡片就地变成**AI 预填的建会话表单**——确认或微调后点「✅ 创建会话」即可,自动开话题开工:
135
+ - **建会话(目录优先)**:AI 先把**工作目录定下来**,再就地给出**预填表单**——确认或微调后点「✅ 创建会话」即可,自动开话题开工:
136
136
 
137
137
  ```
138
138
  你:帮我修一下 zlib 的下载 bug,用高风险审批
139
- → 📝 建会话表单(AI 已预填:目录 `/Users/code/zlib`、权限「高风险审批」)
140
- [✅ 创建会话] ← 确认后建会话 + 自动开话题
139
+ → 📝 建会话表单(目录 `/Users/code/zlib` ✓ 匹配历史目录;权限「高风险审批」)
140
+ [✅ 创建会话]
141
+
142
+ 你:股票研究
143
+ → 📝 建会话表单(目录 `/Users/code/stock-research` ➕ AI 新建,不存在时会在创建时自动创建)
144
+ [✅ 创建会话]
141
145
  ```
142
146
 
147
+ 目录决策顺序:① 你明确给的路径 → ② 语义匹配**现成目录**(AI 会先看**允许根目录的一级子目录**,再看最近使用 / 历史会话目录)→ ③ 都不匹配则**按主题在允许根目录下新建**(`<允许根目录>/<英文短横线主题>`)→ ④ 兜底允许根目录。**表单永远带目录**,不会出现空目录。
148
+
143
149
  - **列会话**:说「我有哪些会话」→ 直接出会话列表卡(等同 `/sessions`,可翻页 / 进话题 / 新建);
144
150
  - **闲聊 / 其它**:照旧回管理台提示卡。
145
151
 
146
- 防误伤:目录必须命中候选(最近使用 + 本机会话目录)且通过 allowedRoots 复核、模型必须命中可选列表;AI 解析不了的字段**留空由你在表单里补**,最终一定要经过表单确认。`quickNew: false` 可整体关闭。
152
+ 防误伤:AI 新造的路径必须落在允许范围(`allowedRoots`)内、模型必须命中可选列表;**你明确指定但越界的路径不会静默替换**——表单不预填并给出警示,由你修改。最终一定经过表单确认。`quickNew: false` 可整体关闭。
147
153
 
148
154
  ### 话题内(干活)
149
155
 
@@ -229,6 +235,22 @@ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发
229
235
  - [x] **接收图片 / 文件**:已支持——自动下载到**会话工作目录**下的 `.opencode/temp/opencode-feishu-plugin/`(内置 `.gitignore`,不污染 `git status`;可用 `attachmentsDir` 覆盖;单附件默认 ≤20MB)。
230
236
  - [x] **忙时新消息默认插队**:已支持——忙碌时新消息**默认直接插队**(打断当前步骤优先执行);`busyDelivery: "queue"` 可切回原生排队(长命令场景更温和)。
231
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)。
253
+
232
254
  ## 高级主题与开发
233
255
 
234
256
  安全设计、位置保活、卡片守卫、会话恢复、多实例网关选举、完整配置项与开发架构见 [docs/advanced.md](./docs/advanced.md)。
package/dist/index.js CHANGED
@@ -148586,23 +148586,33 @@ function buildQuickNewThinkingCard() {
148586
148586
 
148587
148587
  // src/session/quick-new.ts
148588
148588
  var QUICK_NEW_PERMS = ["readonly", "edit", "askHigh", "trust"];
148589
+ var QUICK_NEW_DIR_SOURCES = ["given", "existing", "new"];
148589
148590
  var QUICK_NEW_INSTRUCTION = [
148590
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",
148591
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",
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
+ '{"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>"}',
148593
148594
  "\u89C4\u5219\uFF1A",
148594
148595
  "- \u5217\u51FA/\u67E5\u770B\u4F1A\u8BDD \u2192 intent=list\uFF0C\u5176\u4F59\u5B57\u6BB5\u7559\u7A7A\u3002",
148595
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",
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
+ "- **\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',
148597
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",
148598
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"
148599
148605
  ].join("\n");
148600
148606
  function buildQuickNewPrompt(input) {
148601
- const dirs = input.candidates.slice(0, 40).map((c) => `- ${c.path}${c.label ? `\uFF08${c.label}\uFF09` : ""}`).join("\n");
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");
148602
148609
  const models = (input.models ?? []).slice(0, 30).map((m) => `- ${m.providerID}/${m.id}${m.name ? `\uFF08${m.name}\uFF09` : ""}`).join("\n");
148603
148610
  return [
148604
148611
  QUICK_NEW_INSTRUCTION,
148605
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
+ "",
148606
148616
  "\u5019\u9009\u76EE\u5F55\uFF1A",
148607
148617
  dirs || "\uFF08\u65E0\uFF09",
148608
148618
  "",
@@ -148616,6 +148626,9 @@ function buildQuickNewPrompt(input) {
148616
148626
  function isPerm(value3) {
148617
148627
  return typeof value3 === "string" && QUICK_NEW_PERMS.includes(value3);
148618
148628
  }
148629
+ function isDirSource(value3) {
148630
+ return typeof value3 === "string" && QUICK_NEW_DIR_SOURCES.includes(value3);
148631
+ }
148619
148632
  function parseQuickNewDecision(raw) {
148620
148633
  if (!raw) return void 0;
148621
148634
  const text = raw.trim();
@@ -148633,6 +148646,7 @@ function parseQuickNewDecision(raw) {
148633
148646
  const intent = obj.intent === "create" || obj.intent === "list" || obj.intent === "chat" ? obj.intent : void 0;
148634
148647
  if (!intent) return void 0;
148635
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;
148636
148650
  const title = typeof obj.title === "string" ? obj.title.trim().slice(0, 30) : "";
148637
148651
  const perm = isPerm(obj.perm) ? obj.perm : void 0;
148638
148652
  const model = typeof obj.model === "string" ? obj.model.trim() : "";
@@ -148640,12 +148654,16 @@ function parseQuickNewDecision(raw) {
148640
148654
  return {
148641
148655
  intent,
148642
148656
  ...dir ? { directory: dir } : {},
148657
+ ...dirSource ? { dirSource } : {},
148643
148658
  ...title ? { title } : {},
148644
148659
  ...perm ? { perm } : {},
148645
148660
  ...model ? { model } : {},
148646
148661
  ...reason ? { reason } : {}
148647
148662
  };
148648
148663
  }
148664
+ function slugifyTitle(title) {
148665
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/g, "");
148666
+ }
148649
148667
  function matchCandidateDirectory(directory, candidates) {
148650
148668
  if (!directory) return void 0;
148651
148669
  const normalize2 = (p) => p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
@@ -148682,7 +148700,7 @@ function isUnder(child, root) {
148682
148700
  function isSystemPath(path2) {
148683
148701
  return SYSTEM_DIRS.some((dir) => isUnder(path2, dir));
148684
148702
  }
148685
- function validateDirectory(rawPath, allowedRoots, deps = {}) {
148703
+ function validateDirectory(rawPath, allowedRoots, deps = {}, opts = {}) {
148686
148704
  const raw = rawPath.trim();
148687
148705
  const roots = allowedRoots.map((r) => resolve$1(r)).filter((r) => r && r !== "");
148688
148706
  if (roots.length === 0) {
@@ -148710,10 +148728,12 @@ function validateDirectory(rawPath, allowedRoots, deps = {}) {
148710
148728
  exists2 = false;
148711
148729
  }
148712
148730
  if (!exists2) {
148713
- try {
148714
- mkdir2(target, { recursive: true });
148715
- } catch {
148716
- 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
+ }
148717
148737
  }
148718
148738
  }
148719
148739
  let real = target;
@@ -148740,6 +148760,33 @@ function forbiddenReason(path2) {
148740
148760
  function err(reason, message) {
148741
148761
  return { ok: false, reason, message };
148742
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
+ }
148743
148790
 
148744
148791
  // src/feishu/perm-presets.ts
148745
148792
  var SHELL_ACTIONS = ["shell", "bash"];
@@ -149573,6 +149620,9 @@ function buildSetupFormCard(input) {
149573
149620
  if (input.error) {
149574
149621
  formElements.push({ tag: "markdown", content: truncateCardContent(`\u26A0\uFE0F **\u63D0\u4EA4\u5931\u8D25**\uFF1A${input.error}`) });
149575
149622
  }
149623
+ if (input.notice) {
149624
+ formElements.push({ tag: "markdown", content: truncateCardContent(input.notice) });
149625
+ }
149576
149626
  formElements.push(
149577
149627
  { tag: "markdown", content: truncateCardContent(lines.join("\n")) },
149578
149628
  {
@@ -149805,33 +149855,6 @@ function createSessionContext(deps) {
149805
149855
  };
149806
149856
  return ctx;
149807
149857
  }
149808
- var MAX_ROOT_SUBDIRS = 15;
149809
- async function scanRootSubdirs(root, deps = {}) {
149810
- const rootPath = root.trim();
149811
- if (!rootPath) return [];
149812
- const rd = deps.readdir ?? ((path2, options2) => readdir(path2, options2));
149813
- let entries3;
149814
- try {
149815
- entries3 = await rd(rootPath, { withFileTypes: true });
149816
- } catch {
149817
- return [];
149818
- }
149819
- 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);
149820
- const out = [];
149821
- for (const name of names) {
149822
- const path2 = join(rootPath, name);
149823
- out.push({ name, path: path2, isRepo: await hasGit(path2, rd) });
149824
- }
149825
- return out;
149826
- }
149827
- async function hasGit(path2, rd) {
149828
- try {
149829
- const entries3 = await rd(path2, { withFileTypes: true });
149830
- return entries3.some((e) => e.name === ".git");
149831
- } catch {
149832
- return false;
149833
- }
149834
- }
149835
149858
 
149836
149859
  // src/session/model-perm.ts
149837
149860
  async function currentModel(ctx, sessionID) {
@@ -150024,7 +150047,9 @@ async function buildPrefilledSetupForm(ctx, chatId, anchorMessageId, prefill) {
150024
150047
  ...prefill.perm !== void 0 ? { perm: prefill.perm } : {}
150025
150048
  };
150026
150049
  await ctx.deps.wizard.set(chatId, next);
150027
- return renderFormCard(ctx, next);
150050
+ return renderFormCard(ctx, next, {
150051
+ ...prefill.notice ? { notice: prefill.notice } : {}
150052
+ });
150028
150053
  }
150029
150054
  async function cmdDir(ctx, message, args2) {
150030
150055
  if (message.threadId) {
@@ -150389,6 +150414,7 @@ async function renderFormCard(ctx, state, over) {
150389
150414
  ...rootSubdirs.length > 0 ? { rootSubdirs } : {},
150390
150415
  ...ctx.deps.allowedRoots ? { allowedRoots: ctx.deps.allowedRoots } : {},
150391
150416
  ...over?.error ? { error: over.error } : {},
150417
+ ...over?.notice ? { notice: over.notice } : {},
150392
150418
  values: values2
150393
150419
  });
150394
150420
  }
@@ -152485,6 +152511,42 @@ async function start(ctx, config, log2, logSink) {
152485
152511
  log2.info("\u673A\u5668\u4EBA\u83DC\u5355\u70B9\u51FB \u2192 \u6267\u884C\u547D\u4EE4", { eventKey: click.eventKey, cmd: entry.cmd, label: entry.label });
152486
152512
  await handleMessage(synthetic);
152487
152513
  }
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" };
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();
152549
+ }
152488
152550
  async function handleQuickNew(message) {
152489
152551
  const hint = async () => {
152490
152552
  const res = await sender.sendCard(message.chatId, buildConsoleHintCard());
@@ -152493,12 +152555,12 @@ async function start(ctx, config, log2, logSink) {
152493
152555
  try {
152494
152556
  const candidates = [];
152495
152557
  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
- }
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
+ };
152502
152564
  let entries3 = [];
152503
152565
  try {
152504
152566
  const raw = await listAllSessionsRaw() ?? await listSessionsOverHttp({}, { log: log2 });
@@ -152506,12 +152568,24 @@ async function start(ctx, config, log2, logSink) {
152506
152568
  } catch (err2) {
152507
152569
  log2.debug("quick-new \u4F1A\u8BDD\u5217\u8868\u8BFB\u53D6\u5931\u8D25", { error: errorMessage(err2) });
152508
152570
  }
152571
+ const sessionLabelByDir = /* @__PURE__ */ new Map();
152509
152572
  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 } : {} });
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) });
152513
152584
  }
152514
152585
  }
152586
+ for (const entry of entries3) {
152587
+ if (entry.directory) pushCandidate(entry.directory, entry.title);
152588
+ }
152515
152589
  const routingSessionID = entries3[0]?.sessionID;
152516
152590
  if (!routingSessionID) {
152517
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");
@@ -152542,7 +152616,12 @@ async function start(ctx, config, log2, logSink) {
152542
152616
  const outcome = await quickGenerateWithSession(
152543
152617
  { log: log2, generateText },
152544
152618
  {
152545
- prompt: buildQuickNewPrompt({ text: message.text, candidates, models }),
152619
+ prompt: buildQuickNewPrompt({
152620
+ text: message.text,
152621
+ candidates,
152622
+ models,
152623
+ allowedRoots: config.allowedRoots
152624
+ }),
152546
152625
  sessionID: routingSessionID,
152547
152626
  ...genModel ? { model: genModel } : {}
152548
152627
  }
@@ -152565,12 +152644,11 @@ async function start(ctx, config, log2, logSink) {
152565
152644
  log2.info("quick-new\uFF1A\u8BC6\u522B\u5361\u5DF2\u5C31\u5730\u5207\u6362\u4E3A\u4F1A\u8BDD\u5217\u8868\u5361", { chatId: message.chatId });
152566
152645
  return;
152567
152646
  }
152568
- const dir = matchCandidateDirectory(decision.directory, candidates);
152569
- const validation = dir ? validateDir(dir) : void 0;
152647
+ const resolution = resolveConsoleDir(decision, candidates);
152570
152648
  const model = matchModelOption(decision.model, models);
152571
152649
  const prefill = {
152572
152650
  title: decision.title || topicTitle(message.text),
152573
- ...validation?.ok ? { dir: validation.path } : {},
152651
+ ...resolution.dir ? { dir: resolution.dir } : {},
152574
152652
  ...model ? {
152575
152653
  model: {
152576
152654
  providerID: model.providerID,
@@ -152578,13 +152656,15 @@ async function start(ctx, config, log2, logSink) {
152578
152656
  ...model.name ? { name: model.name } : {}
152579
152657
  }
152580
152658
  } : {},
152581
- ...decision.perm ? { perm: decision.perm } : {}
152659
+ ...decision.perm ? { perm: decision.perm } : {},
152660
+ notice: resolution.notice
152582
152661
  };
152583
152662
  const form = await commands.buildPrefilledSetupForm(message.chatId, ack.messageId, prefill);
152584
152663
  await sender.patchCard(ack.messageId, form);
152585
152664
  log2.info("quick-new\uFF1AAI \u9884\u586B\u5EFA\u4F1A\u8BDD\u8868\u5355\u5DF2\u53D1\u9001\uFF08\u5C31\u5730\uFF09", {
152586
152665
  title: prefill.title,
152587
- dir: validation?.ok ? validation.path : void 0,
152666
+ dir: resolution.dir,
152667
+ dirSource: resolution.source,
152588
152668
  perm: decision.perm,
152589
152669
  model: model ? `${model.providerID}/${model.id}` : void 0
152590
152670
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-feishu-plugin",
3
- "version": "0.2.15",
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",