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 +26 -4
- package/README.md +26 -4
- package/dist/index.js +131 -51
- package/package.json +1 -1
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
|
|
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 (
|
|
140
|
-
[✅ 创建会话]
|
|
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:
|
|
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
|
-
-
|
|
135
|
+
- **建会话(目录优先)**:AI 先把**工作目录定下来**,再就地给出**预填表单**——确认或微调后点「✅ 创建会话」即可,自动开话题开工:
|
|
136
136
|
|
|
137
137
|
```
|
|
138
138
|
你:帮我修一下 zlib 的下载 bug,用高风险审批
|
|
139
|
-
→ 📝
|
|
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
|
-
|
|
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
|
|
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
|
-
"-
|
|
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
|
|
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
|
-
|
|
148714
|
-
|
|
148715
|
-
|
|
148716
|
-
|
|
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
|
-
|
|
152497
|
-
|
|
152498
|
-
|
|
152499
|
-
|
|
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 &&
|
|
152511
|
-
|
|
152512
|
-
|
|
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({
|
|
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
|
|
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
|
-
...
|
|
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:
|
|
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.
|
|
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",
|