opencode-feishu-plugin 0.2.14 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +15 -13
- package/README.md +15 -13
- package/dist/index.js +253 -282
- package/package.json +1 -1
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;
|
|
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,22 @@ 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
|
-
###
|
|
131
|
+
### AI session management (main chat)
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
Send plain text in the main chat and the AI judges the intent and handles it:
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
- **Create a session**: it parses directory / title / permission tier / model and turns the card in place into an **AI-prefilled create-session form** — confirm (or tweak) and tap "✅ 创建会话", and a topic opens with work started:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
You: fix the zlib download bug, use high-risk approval
|
|
139
|
+
→ 📝 Create-session form (AI prefilled: dir `/Users/code/zlib`, permission "High-risk approval")
|
|
140
|
+
[✅ 创建会话] ← confirm to create + auto-open a topic
|
|
141
|
+
```
|
|
141
142
|
|
|
142
|
-
|
|
143
|
+
- **List sessions**: say "what sessions do I have?" → a session list card appears directly (same as `/sessions`; paginate / enter / create);
|
|
144
|
+
- **Chit-chat / other**: the usual console hint card.
|
|
143
145
|
|
|
144
|
-
|
|
145
|
-
- Proposal cards are valid for 1 hour; set `quickNew: false` to go back to the console-only mode.
|
|
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.
|
|
146
147
|
|
|
147
148
|
### Inside a topic (work)
|
|
148
149
|
|
|
@@ -192,6 +193,7 @@ When the agent asks via `question` or other form tools, the form becomes a Feish
|
|
|
192
193
|
| `approvalTtlMs` | number | `600000` | Approval token / card validity |
|
|
193
194
|
| `staleExecutionMs` | number | `300000` | Watchdog threshold (0–60 min; **0 = disabled**; sessions waiting on a form / pending approval are never killed) |
|
|
194
195
|
| `quickNew` | boolean | `true` | Main-chat "one-sentence session": AI judges intent + finds the directory, proposal card creates in one tap; `false` = console-only mode |
|
|
196
|
+
| `busyDelivery` | `steer`\|`queue` | `steer` | Delivery for new messages while busy: `steer` = cut in and interrupt the current step; `queue` = native queueing (gentler for long commands) |
|
|
195
197
|
| `gatewayLocation` | string | — | Only start the gateway at this location (or its subdirectories); empty = any location |
|
|
196
198
|
|
|
197
199
|
Full config (including `cardMaxTables`, `topicStatus*`, `resumeSummary*`, `keepalive*`, `gatewayMatchGraceMs`) → [docs/advanced.en.md](./docs/advanced.en.md#full-configuration).
|
|
@@ -225,7 +227,7 @@ Full config (including `cardMaxTables`, `topicStatus*`, `resumeSummary*`, `keepa
|
|
|
225
227
|
Iterating from real usage feedback; current plan:
|
|
226
228
|
|
|
227
229
|
- [x] **Accept images / files**: done — downloaded automatically into the session working directory at `.opencode/temp/opencode-feishu-plugin/` (with a built-in `.gitignore`, so `git status` stays clean; override with `attachmentsDir`; default max 20MB per attachment).
|
|
228
|
-
- [
|
|
230
|
+
- [x] **New messages cut in by default when busy**: done — while busy, new messages **cut in immediately** (interrupting the current step to run first); set `busyDelivery: "queue"` to go back to native queueing (gentler for long-running commands).
|
|
229
231
|
|
|
230
232
|
## Advanced topics & development
|
|
231
233
|
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
| 🚀 **一键建会话** | `/new` 一张表单(目录 + 模型 + 权限档位)一次填完,提交即建会话并自动开话题 |
|
|
21
21
|
| ✅ **卡片审批** | 权限请求变飞书卡片:允许一次 / 始终允许 / 本会话内允许 / 拒绝,自签 token 防伪防重放 |
|
|
22
22
|
| 📊 **实时可见** | 「思考中」回执 → 工具调用实时上卡 → 文本流式更新,页脚显示当前模型 |
|
|
23
|
-
| ⏹ **可控可停** |
|
|
23
|
+
| ⏹ **可控可停** | 每张回复卡带「强制停止」;看门狗自动中断卡死会话;忙时新消息**默认插队**(可配置为排队),`/steer` `/now` 随时可用 |
|
|
24
24
|
| 📎 **图片 / 文件** | 飞书里的图片 / 文件自动下载并挂进会话,支持视觉 / 文件的模型直接看图、读文件 |
|
|
25
25
|
| 🧭 **一句话建会话** | 主聊天流发任务文本,AI 自动判断意图、找好工作目录,一键建会话并开始处理 |
|
|
26
26
|
| 🖱 **主窗口菜单** | 机器人输入框上固定「新建会话」「会话列表」快捷按钮(机器人自定义菜单),一点即用 |
|
|
@@ -128,21 +128,22 @@ tail -f ~/.config/opencode/plugins/feishu.log # 应看到「飞书长连接已
|
|
|
128
128
|
- 菜单配置修改后需**重新发布版本**(官方说明:发版成功后约 5 分钟内生效);
|
|
129
129
|
- 首次使用菜单前,先给机器人发过至少一条消息(插件据消息记住单聊会话;正常使用必然满足)。
|
|
130
130
|
|
|
131
|
-
###
|
|
131
|
+
### AI 会话管理(主聊天流)
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
主聊天流发普通文本,AI 会判断意图并直接处理:
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
- **建会话**:解析 目录 / 标题 / 权限档位 / 模型,把卡片就地变成**AI 预填的建会话表单**——确认或微调后点「✅ 创建会话」即可,自动开话题开工:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
你:帮我修一下 zlib 的下载 bug,用高风险审批
|
|
139
|
+
→ 📝 建会话表单(AI 已预填:目录 `/Users/code/zlib`、权限「高风险审批」)
|
|
140
|
+
[✅ 创建会话] ← 确认后建会话 + 自动开话题
|
|
141
|
+
```
|
|
141
142
|
|
|
142
|
-
|
|
143
|
+
- **列会话**:说「我有哪些会话」→ 直接出会话列表卡(等同 `/sessions`,可翻页 / 进话题 / 新建);
|
|
144
|
+
- **闲聊 / 其它**:照旧回管理台提示卡。
|
|
143
145
|
|
|
144
|
-
|
|
145
|
-
- 建议卡有效期 1 小时;不想要此功能可设 `quickNew: false` 回到纯管理台模式。
|
|
146
|
+
防误伤:目录必须命中候选(最近使用 + 本机会话目录)且通过 allowedRoots 复核、模型必须命中可选列表;AI 解析不了的字段**留空由你在表单里补**,最终一定要经过表单确认。`quickNew: false` 可整体关闭。
|
|
146
147
|
|
|
147
148
|
### 话题内(干活)
|
|
148
149
|
|
|
@@ -192,6 +193,7 @@ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发
|
|
|
192
193
|
| `approvalTtlMs` | number | `600000` | 审批 token / 卡片有效期 |
|
|
193
194
|
| `staleExecutionMs` | number | `300000` | 看门狗阈值(0–60 分钟;**0 = 关闭看门狗**;待答表单 / 未决审批期间不判卡死) |
|
|
194
195
|
| `quickNew` | boolean | `true` | 主聊天流「一句话建会话」:AI 判断意图 + 找目录,建议卡一键创建;`false` = 回到纯管理台模式 |
|
|
196
|
+
| `busyDelivery` | `steer`\|`queue` | `steer` | 忙时新消息投递方式:`steer` = 立即插队打断当前步骤;`queue` = 原生排队(长命令场景更温和) |
|
|
195
197
|
| `gatewayLocation` | string | — | 只在该 location(及其子目录)启动网关;留空 = 任意 location 生效 |
|
|
196
198
|
|
|
197
199
|
完整配置(含 `cardMaxTables`、`topicStatus*`、`resumeSummary*`、`keepalive*`、`gatewayMatchGraceMs` 等进阶项)见 [docs/advanced.md](./docs/advanced.md#完整配置项)。
|
|
@@ -225,7 +227,7 @@ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发
|
|
|
225
227
|
按真实使用反馈迭代,当前规划:
|
|
226
228
|
|
|
227
229
|
- [x] **接收图片 / 文件**:已支持——自动下载到**会话工作目录**下的 `.opencode/temp/opencode-feishu-plugin/`(内置 `.gitignore`,不污染 `git status`;可用 `attachmentsDir` 覆盖;单附件默认 ≤20MB)。
|
|
228
|
-
- [
|
|
230
|
+
- [x] **忙时新消息默认插队**:已支持——忙碌时新消息**默认直接插队**(打断当前步骤优先执行);`busyDelivery: "queue"` 可切回原生排队(长命令场景更温和)。
|
|
229
231
|
|
|
230
232
|
## 高级主题与开发
|
|
231
233
|
|
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 ?
|
|
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,54 @@ 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\
|
|
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
|
-
{
|
|
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"];
|
|
148632
148589
|
var QUICK_NEW_INSTRUCTION = [
|
|
148633
148590
|
"\u4F60\u662F\u98DE\u4E66 AI \u52A9\u624B\u300C\u7BA1\u7406\u53F0\u300D\u7684\u610F\u56FE\u8BC6\u522B\u5668\u3002\u7528\u6237\u5728\u7BA1\u7406\u53F0\uFF08\u8FD8\u6CA1\u6709\u4F1A\u8BDD\uFF09\u53D1\u6765\u4E00\u6761\u6D88\u606F\u3002",
|
|
148634
|
-
"\
|
|
148635
|
-
"\
|
|
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>"}',
|
|
148591
|
+
"\u5224\u65AD\u610F\u56FE\u5E76\u5C3D\u91CF\u89E3\u6790\u5EFA\u4F1A\u8BDD\u5B57\u6BB5\u3002\u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\uFF1A",
|
|
148592
|
+
'{"intent":"create|list|chat","dir":"<\u7EDD\u5BF9\u8DEF\u5F84\u6216\u7A7A\u5B57\u7B26\u4E32>","title":"<\u4E0D\u8D85\u8FC720\u5B57\u7684\u4F1A\u8BDD\u6807\u9898>","perm":"readonly|edit|askHigh|trust|\u7A7A","model":"<providerID/modelID \u6216\u7A7A>","reason":"<\u4E00\u53E5\u8BDD\u7406\u7531>"}',
|
|
148637
148593
|
"\u89C4\u5219\uFF1A",
|
|
148638
|
-
"- \
|
|
148639
|
-
"- \
|
|
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"
|
|
148594
|
+
"- \u5217\u51FA/\u67E5\u770B\u4F1A\u8BDD \u2192 intent=list\uFF0C\u5176\u4F59\u5B57\u6BB5\u7559\u7A7A\u3002",
|
|
148595
|
+
"- \u9700\u8981\u65B0\u5EFA\u4F1A\u8BDD\u6267\u884C\u7684\u5F00\u53D1/\u64CD\u4F5C\u4EFB\u52A1 \u2192 intent=create\uFF1B\u95F2\u804A\u3001\u95EE\u5019\u3001\u8BE2\u95EE\u7528\u6CD5 \u2192 chat\u3002",
|
|
148596
|
+
"- dir \u53EA\u80FD\u4ECE\u5019\u9009\u76EE\u5F55\u4E2D\u9009\u62E9\uFF08\u8BED\u4E49\u6700\u5339\u914D\u7684\u90A3\u4E2A\uFF09\uFF1B\u627E\u4E0D\u5230\u5408\u9002\u7684\u5C31\u7559\u7A7A\u5B57\u7B26\u4E32\u3002",
|
|
148597
|
+
"- perm \u4F9D\u636E\u7528\u6237\u8868\u8FF0\uFF08\u53EA\u8BFB\u2192readonly\u3001\u53EF\u7F16\u8F91\u2192edit\u3001\u9AD8\u98CE\u9669\u2192askHigh\u3001\u5B8C\u5168\u4FE1\u4EFB\u2192trust\uFF09\uFF1B\u7528\u6237\u6CA1\u8BF4\u5C31\u7559\u7A7A\u3002",
|
|
148598
|
+
"- model \u53EA\u80FD\u4ECE\u5019\u9009\u6A21\u578B\u4E2D\u7CBE\u786E\u590D\u5236 providerID/modelID\uFF1B\u7528\u6237\u6CA1\u8BF4\u5C31\u7559\u7A7A\u3002"
|
|
148641
148599
|
].join("\n");
|
|
148642
|
-
function buildQuickNewPrompt(
|
|
148643
|
-
const
|
|
148600
|
+
function buildQuickNewPrompt(input) {
|
|
148601
|
+
const dirs = input.candidates.slice(0, 40).map((c) => `- ${c.path}${c.label ? `\uFF08${c.label}\uFF09` : ""}`).join("\n");
|
|
148602
|
+
const models = (input.models ?? []).slice(0, 30).map((m) => `- ${m.providerID}/${m.id}${m.name ? `\uFF08${m.name}\uFF09` : ""}`).join("\n");
|
|
148644
148603
|
return [
|
|
148645
148604
|
QUICK_NEW_INSTRUCTION,
|
|
148646
148605
|
"",
|
|
148647
148606
|
"\u5019\u9009\u76EE\u5F55\uFF1A",
|
|
148648
|
-
|
|
148607
|
+
dirs || "\uFF08\u65E0\uFF09",
|
|
148608
|
+
"",
|
|
148609
|
+
"\u5019\u9009\u6A21\u578B\uFF1A",
|
|
148610
|
+
models || "\uFF08\u65E0\uFF09",
|
|
148649
148611
|
"",
|
|
148650
148612
|
"\u7528\u6237\u6D88\u606F\uFF1A",
|
|
148651
|
-
text.slice(0, 2e3)
|
|
148613
|
+
input.text.slice(0, 2e3)
|
|
148652
148614
|
].join("\n");
|
|
148653
148615
|
}
|
|
148616
|
+
function isPerm(value3) {
|
|
148617
|
+
return typeof value3 === "string" && QUICK_NEW_PERMS.includes(value3);
|
|
148618
|
+
}
|
|
148654
148619
|
function parseQuickNewDecision(raw) {
|
|
148655
148620
|
if (!raw) return void 0;
|
|
148656
148621
|
const text = raw.trim();
|
|
@@ -148665,15 +148630,19 @@ function parseQuickNewDecision(raw) {
|
|
|
148665
148630
|
}
|
|
148666
148631
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
148667
148632
|
const obj = parsed;
|
|
148668
|
-
const intent = obj.intent === "
|
|
148633
|
+
const intent = obj.intent === "create" || obj.intent === "list" || obj.intent === "chat" ? obj.intent : void 0;
|
|
148669
148634
|
if (!intent) return void 0;
|
|
148670
148635
|
const dir = typeof obj.dir === "string" ? obj.dir.trim() : typeof obj.directory === "string" ? obj.directory.trim() : "";
|
|
148671
148636
|
const title = typeof obj.title === "string" ? obj.title.trim().slice(0, 30) : "";
|
|
148637
|
+
const perm = isPerm(obj.perm) ? obj.perm : void 0;
|
|
148638
|
+
const model = typeof obj.model === "string" ? obj.model.trim() : "";
|
|
148672
148639
|
const reason = typeof obj.reason === "string" ? obj.reason.trim().slice(0, 200) : "";
|
|
148673
148640
|
return {
|
|
148674
148641
|
intent,
|
|
148675
148642
|
...dir ? { directory: dir } : {},
|
|
148676
148643
|
...title ? { title } : {},
|
|
148644
|
+
...perm ? { perm } : {},
|
|
148645
|
+
...model ? { model } : {},
|
|
148677
148646
|
...reason ? { reason } : {}
|
|
148678
148647
|
};
|
|
148679
148648
|
}
|
|
@@ -148684,6 +148653,12 @@ function matchCandidateDirectory(directory, candidates) {
|
|
|
148684
148653
|
const hit = candidates.find((c) => normalize2(c.path) === target);
|
|
148685
148654
|
return hit?.path;
|
|
148686
148655
|
}
|
|
148656
|
+
function matchModelOption(model, models) {
|
|
148657
|
+
if (!model) return void 0;
|
|
148658
|
+
const norm = (s) => s.trim().toLowerCase();
|
|
148659
|
+
const target = norm(model);
|
|
148660
|
+
return models.find((m) => norm(`${m.providerID}/${m.id}`) === target) ?? models.find((m) => norm(m.id) === target) ?? models.find((m) => m.name !== void 0 && norm(m.name) === target);
|
|
148661
|
+
}
|
|
148687
148662
|
var SYSTEM_DIRS = [
|
|
148688
148663
|
"/etc",
|
|
148689
148664
|
"/usr",
|
|
@@ -149435,7 +149410,7 @@ var SETUP_FORM_FIELDS = {
|
|
|
149435
149410
|
var SETUP_FORM_MAX_MODELS = 15;
|
|
149436
149411
|
var SETUP_FORM_DEFAULT_PERM = "edit";
|
|
149437
149412
|
var SETUP_FORM_DIR_CUSTOM = "__custom__";
|
|
149438
|
-
function
|
|
149413
|
+
function button3(text, type, value3) {
|
|
149439
149414
|
return {
|
|
149440
149415
|
tag: "button",
|
|
149441
149416
|
text: { tag: "plain_text", content: text },
|
|
@@ -149478,28 +149453,28 @@ function buildModelCard(input) {
|
|
|
149478
149453
|
if (input.page <= 0 && recent.length > 0) {
|
|
149479
149454
|
for (const model of recent) {
|
|
149480
149455
|
const isCurrent = input.current && refKey(input.current) === refKey(model);
|
|
149481
|
-
elements.push(
|
|
149456
|
+
elements.push(button3(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, isCurrent ? "primary" : "default", modelValue(model, input.sid)));
|
|
149482
149457
|
}
|
|
149483
149458
|
if (input.models.length > recent.length) {
|
|
149484
|
-
elements.push(
|
|
149459
|
+
elements.push(button3("\u66F4\u591A \u25B8", "default", moreValue(1, input.sid)));
|
|
149485
149460
|
}
|
|
149486
149461
|
} else {
|
|
149487
149462
|
const pageNum = input.page <= 0 ? 1 : input.page;
|
|
149488
149463
|
const slice = input.models.slice((pageNum - 1) * pageSize, pageNum * pageSize);
|
|
149489
149464
|
if (slice.length === 0 && recent.length > 0) {
|
|
149490
149465
|
for (const model of recent) {
|
|
149491
|
-
elements.push(
|
|
149466
|
+
elements.push(button3(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, "default", modelValue(model, input.sid)));
|
|
149492
149467
|
}
|
|
149493
149468
|
} else {
|
|
149494
149469
|
for (const model of slice) {
|
|
149495
149470
|
const isCurrent = input.current && refKey(input.current) === refKey(model);
|
|
149496
149471
|
elements.push(
|
|
149497
|
-
|
|
149472
|
+
button3(`\u{1F9E0} ${shorten(modelLabel(model), 30)}`, isCurrent ? "primary" : "default", modelValue(model, input.sid))
|
|
149498
149473
|
);
|
|
149499
149474
|
}
|
|
149500
|
-
if (pageNum > 1) elements.push(
|
|
149475
|
+
if (pageNum > 1) elements.push(button3("\u25C2 \u4E0A\u4E00\u9875", "default", moreValue(pageNum - 1, input.sid)));
|
|
149501
149476
|
if (pageNum * pageSize < input.models.length) {
|
|
149502
|
-
elements.push(
|
|
149477
|
+
elements.push(button3("\u4E0B\u4E00\u9875 \u25B8", "default", moreValue(pageNum + 1, input.sid)));
|
|
149503
149478
|
}
|
|
149504
149479
|
}
|
|
149505
149480
|
}
|
|
@@ -149513,7 +149488,7 @@ function buildPermCard(input) {
|
|
|
149513
149488
|
}
|
|
149514
149489
|
const elements = [{ tag: "markdown", content: truncateCardContent(lines.join("\n")) }];
|
|
149515
149490
|
for (const info of PERMISSION_PRESETS) {
|
|
149516
|
-
elements.push(
|
|
149491
|
+
elements.push(button3(presetLabel(info.id), input.current === info.id ? "primary" : "default", permValue(info.id, input.sid)));
|
|
149517
149492
|
}
|
|
149518
149493
|
return headerCard("\u{1F510} \u9009\u62E9\u6743\u9650\u6863\u4F4D", "orange", elements);
|
|
149519
149494
|
}
|
|
@@ -149529,8 +149504,8 @@ function buildConfirmCard(input) {
|
|
|
149529
149504
|
];
|
|
149530
149505
|
return headerCard("\u2705 \u786E\u8BA4\u521B\u5EFA\u4F1A\u8BDD", "green", [
|
|
149531
149506
|
{ tag: "markdown", content: truncateCardContent(lines.join("\n")) },
|
|
149532
|
-
|
|
149533
|
-
|
|
149507
|
+
button3("\u2705 \u521B\u5EFA", "primary", { wizard: "confirm" }),
|
|
149508
|
+
button3("\u2716\uFE0F \u53D6\u6D88", "default", { wizard: "cancel" })
|
|
149534
149509
|
]);
|
|
149535
149510
|
}
|
|
149536
149511
|
function buildSetupDoneCard(title, lines, template2 = "grey") {
|
|
@@ -150038,6 +150013,19 @@ async function openSetupForm(ctx, message, title) {
|
|
|
150038
150013
|
const res = await ctx.deps.sender.sendCard(message.chatId, card);
|
|
150039
150014
|
if (!res.ok) ctx.deps.log.warn("\u8868\u5355\u5361\u53D1\u9001\u5931\u8D25", { chatId: message.chatId, error: res.error ?? "unknown" });
|
|
150040
150015
|
}
|
|
150016
|
+
async function buildPrefilledSetupForm(ctx, chatId, anchorMessageId, prefill) {
|
|
150017
|
+
let state = await ctx.deps.wizard.get(chatId);
|
|
150018
|
+
if (!state) state = await ctx.deps.wizard.start(chatId, prefill.title, anchorMessageId);
|
|
150019
|
+
const next = {
|
|
150020
|
+
...state,
|
|
150021
|
+
...prefill.title !== void 0 ? { title: prefill.title } : {},
|
|
150022
|
+
...prefill.dir !== void 0 ? { dir: prefill.dir } : {},
|
|
150023
|
+
...prefill.model !== void 0 ? { model: prefill.model } : {},
|
|
150024
|
+
...prefill.perm !== void 0 ? { perm: prefill.perm } : {}
|
|
150025
|
+
};
|
|
150026
|
+
await ctx.deps.wizard.set(chatId, next);
|
|
150027
|
+
return renderFormCard(ctx, next);
|
|
150028
|
+
}
|
|
150041
150029
|
async function cmdDir(ctx, message, args2) {
|
|
150042
150030
|
if (message.threadId) {
|
|
150043
150031
|
await ctx.reply(message, threadForbiddenText("dir"));
|
|
@@ -150432,7 +150420,8 @@ function createSetupWizardApi(ctx) {
|
|
|
150432
150420
|
cmdCancel: (message) => cmdCancel(ctx, message),
|
|
150433
150421
|
applySetupCardAction: (action, value3) => applySetupCardAction(ctx, action, value3),
|
|
150434
150422
|
applySetupFormSubmit: (action) => applySetupFormSubmit(ctx, action),
|
|
150435
|
-
sendSetupFormForChat: (chatId, anchorMessageId, openId) => sendSetupFormForChat(ctx, chatId, anchorMessageId, openId)
|
|
150423
|
+
sendSetupFormForChat: (chatId, anchorMessageId, openId) => sendSetupFormForChat(ctx, chatId, anchorMessageId, openId),
|
|
150424
|
+
buildPrefilledSetupForm: (chatId, anchorMessageId, prefill) => buildPrefilledSetupForm(ctx, chatId, anchorMessageId, prefill)
|
|
150436
150425
|
};
|
|
150437
150426
|
}
|
|
150438
150427
|
|
|
@@ -150639,10 +150628,17 @@ async function patchResumeSummary(ctx, sessionID, dir, messageId, cardInput, per
|
|
|
150639
150628
|
...dir ? { directory: dir } : {},
|
|
150640
150629
|
timeoutMs: ctx.deps.resumeSummaryTimeoutMs ?? 15e3
|
|
150641
150630
|
});
|
|
150631
|
+
const base2 = await ctx.deps.sessionMap.getRootCard(sessionID);
|
|
150632
|
+
if (base2?.compactPending || base2?.summaryLabel === "\u5DF2\u538B\u7F29 \xB7 \u4F1A\u8BDD\u6458\u8981") {
|
|
150633
|
+
if (base2.summaryPending) {
|
|
150634
|
+
await ctx.deps.sessionMap.setRootCard(sessionID, { ...base2, summaryPending: false });
|
|
150635
|
+
}
|
|
150636
|
+
ctx.deps.log.info("\u6062\u590D\u5361\u6458\u8981\u8BA9\u4F4D\u538B\u7F29\u6D41\u7A0B\uFF08\u8DF3\u8FC7 patch\uFF09", { sessionID, source: outcome.source });
|
|
150637
|
+
return;
|
|
150638
|
+
}
|
|
150642
150639
|
const summary = outcome.summary ?? "\uFF08\u6458\u8981\u751F\u6210\u5931\u8D25\uFF0C\u53EF\u76F4\u63A5\u53D1\u6D88\u606F\u7EE7\u7EED\uFF09";
|
|
150643
150640
|
const summaryLabel = SUMMARY_LABEL[outcome.source] ?? "\u6458\u8981";
|
|
150644
150641
|
if (persistBase) {
|
|
150645
|
-
const base2 = await ctx.deps.sessionMap.getRootCard(sessionID);
|
|
150646
150642
|
if (base2) {
|
|
150647
150643
|
await ctx.deps.sessionMap.setRootCard(sessionID, { ...base2, summaryPending: false, summary, summaryLabel });
|
|
150648
150644
|
}
|
|
@@ -150936,6 +150932,19 @@ var SessionCommands = class {
|
|
|
150936
150932
|
);
|
|
150937
150933
|
this.ctx = ctx;
|
|
150938
150934
|
}
|
|
150935
|
+
/** AI 管理台:构建会话列表卡(不发送;调用方自行 patch / 发送)。 */
|
|
150936
|
+
async buildSessionListCard(chatId, page = 0) {
|
|
150937
|
+
const entries3 = await this.ctx.loadSessionEntries(chatId);
|
|
150938
|
+
const active = await this.ctx.deps.sessionMap.getActive(chatId);
|
|
150939
|
+
return this.ctx.buildListCard(chatId, entries3, page, active?.sessionID);
|
|
150940
|
+
}
|
|
150941
|
+
/**
|
|
150942
|
+
* AI 管理台:构建 AI 预填的建会话表单卡(写入向导状态,**不发送**)。
|
|
150943
|
+
* 表单消息 id 即后续话题锚点;用户确认/修改后提交走既有表单流程。
|
|
150944
|
+
*/
|
|
150945
|
+
async buildPrefilledSetupForm(chatId, anchorMessageId, prefill) {
|
|
150946
|
+
return this.ctx.buildPrefilledSetupForm(chatId, anchorMessageId, prefill);
|
|
150947
|
+
}
|
|
150939
150948
|
/**
|
|
150940
150949
|
* 文本命令拦截。返回 true 表示已作为命令处理(调用方**不得**再发 prompt)。
|
|
150941
150950
|
* 非命令返回 false。
|
|
@@ -151520,8 +151529,7 @@ function routeCardAction(action, deps) {
|
|
|
151520
151529
|
return deps.handleCompact(action);
|
|
151521
151530
|
}
|
|
151522
151531
|
const hasForm = action.formValue !== void 0;
|
|
151523
|
-
const routed = hasForm || isSetupFormAction(value3) || Boolean(parseSessionCardValue(value3)) || Boolean(parseSetupCardValue(value3))
|
|
151524
|
-
Boolean(parseQuickNewActionValue(value3));
|
|
151532
|
+
const routed = hasForm || isSetupFormAction(value3) || Boolean(parseSessionCardValue(value3)) || Boolean(parseSetupCardValue(value3));
|
|
151525
151533
|
deps.log.debug("\u5361\u7247\u56DE\u8C03\u8DEF\u7531", {
|
|
151526
151534
|
hasForm,
|
|
151527
151535
|
hasValue: value3 !== void 0,
|
|
@@ -151697,6 +151705,7 @@ async function compactSessionHttp(sessionID, directory, deps) {
|
|
|
151697
151705
|
}
|
|
151698
151706
|
|
|
151699
151707
|
// src/session/quick-generate.ts
|
|
151708
|
+
var TEMP_SESSION_TITLE = "\u2699\uFE0F \u5185\u90E8\u751F\u6210\uFF08\u4E34\u65F6\uFF0C\u53EF\u5FFD\u7565\uFF09";
|
|
151700
151709
|
function sessionRoutingHeaders(sessionID, directory) {
|
|
151701
151710
|
const headers = { "x-opencode-session": sessionID };
|
|
151702
151711
|
if (directory) headers["x-opencode-directory"] = encodeURIComponent(directory);
|
|
@@ -151707,12 +151716,38 @@ function authHeaders3(service3) {
|
|
|
151707
151716
|
const token = Buffer.from(`opencode:${service3.password}`, "utf8").toString("base64");
|
|
151708
151717
|
return { authorization: `Basic ${token}` };
|
|
151709
151718
|
}
|
|
151719
|
+
function sessionChannelEnabled(value3) {
|
|
151720
|
+
if (value3 === "on") return true;
|
|
151721
|
+
if (value3 === "off") return false;
|
|
151722
|
+
const env = process.env;
|
|
151723
|
+
return env["VITEST"] === void 0 && env["NODE_ENV"] !== "test";
|
|
151724
|
+
}
|
|
151725
|
+
function httpChannelAllowed(deps) {
|
|
151726
|
+
if (deps.discover || deps.fetchImpl) return true;
|
|
151727
|
+
const env = process.env;
|
|
151728
|
+
return env["VITEST"] === void 0 && env["NODE_ENV"] !== "test";
|
|
151729
|
+
}
|
|
151710
151730
|
async function quickGenerateWithSession(deps, input) {
|
|
151731
|
+
const httpAllowed = httpChannelAllowed(deps);
|
|
151732
|
+
const discover = deps.discover ?? (() => discoverLocalService());
|
|
151733
|
+
const service3 = httpAllowed ? await discover().catch(() => void 0) : void 0;
|
|
151734
|
+
if (service3 && sessionChannelEnabled(deps.sessionChannel)) {
|
|
151735
|
+
try {
|
|
151736
|
+
const result4 = await generateViaTemporarySession(deps, input, service3);
|
|
151737
|
+
deps.log.info("\u5FEB\u6458\u8981\u751F\u6210\u8D70\u4E34\u65F6\u4F1A\u8BDD", { sessionID: input.sessionID, route: "session" });
|
|
151738
|
+
return { route: "session", result: result4 };
|
|
151739
|
+
} catch (err2) {
|
|
151740
|
+
deps.log.debug("\u4E34\u65F6\u4F1A\u8BDD\u751F\u6210\u5931\u8D25\uFF0C\u56DE\u9000\u751F\u6210 API / \u672C\u673A HTTP", {
|
|
151741
|
+
sessionID: input.sessionID,
|
|
151742
|
+
error: errorMessage(err2)
|
|
151743
|
+
});
|
|
151744
|
+
}
|
|
151745
|
+
}
|
|
151711
151746
|
const headers = sessionRoutingHeaders(input.sessionID, input.directory);
|
|
151712
151747
|
if (deps.generateText) {
|
|
151713
151748
|
try {
|
|
151714
151749
|
const result4 = await withAttemptTimeout(
|
|
151715
|
-
deps.generateText(input.prompt, { headers }),
|
|
151750
|
+
deps.generateText(input.prompt, { headers }, input.model),
|
|
151716
151751
|
deps.attemptTimeoutMs ?? 8e3
|
|
151717
151752
|
);
|
|
151718
151753
|
if (result4 !== void 0 && result4 !== null) {
|
|
@@ -151727,14 +151762,20 @@ async function quickGenerateWithSession(deps, input) {
|
|
|
151727
151762
|
});
|
|
151728
151763
|
}
|
|
151729
151764
|
}
|
|
151730
|
-
const discover = deps.discover ?? (() => discoverLocalService());
|
|
151731
151765
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
151732
|
-
|
|
151733
|
-
|
|
151766
|
+
if (!service3) {
|
|
151767
|
+
throw new Error(
|
|
151768
|
+
httpAllowed ? "\u751F\u6210 API \u4E0E HTTP \u901A\u9053\u5747\u4E0D\u53EF\u7528\uFF08\u672A\u53D1\u73B0\u672C\u673A opencode \u670D\u52A1\u6CE8\u518C\uFF09" : "\u6D4B\u8BD5\u73AF\u5883\u5DF2\u7981\u7528\u672C\u673A HTTP \u751F\u6210\u901A\u9053\uFF08\u672A\u663E\u5F0F\u6CE8\u5165 discover/fetchImpl\uFF09"
|
|
151769
|
+
);
|
|
151770
|
+
}
|
|
151734
151771
|
const response2 = await doFetch(`${service3.url}/api/experimental/generate`, {
|
|
151735
151772
|
method: "POST",
|
|
151736
151773
|
headers: { "content-type": "application/json", ...authHeaders3(service3), ...headers },
|
|
151737
|
-
body: JSON.stringify({
|
|
151774
|
+
body: JSON.stringify({
|
|
151775
|
+
prompt: input.prompt,
|
|
151776
|
+
// 显式模型:未指定时服务端依赖"受支持的默认模型",实际环境常见 400。
|
|
151777
|
+
...input.model ? { model: input.model } : {}
|
|
151778
|
+
}),
|
|
151738
151779
|
signal: AbortSignal.timeout(deps.timeoutMs ?? 15e3)
|
|
151739
151780
|
});
|
|
151740
151781
|
if (!response2.ok) {
|
|
@@ -151748,6 +151789,56 @@ async function quickGenerateWithSession(deps, input) {
|
|
|
151748
151789
|
deps.log.info("\u5FEB\u6458\u8981\u751F\u6210\u8D70\u672C\u673A HTTP \u751F\u6210", { sessionID: input.sessionID, route: "http" });
|
|
151749
151790
|
return { route: "http", result: result3 };
|
|
151750
151791
|
}
|
|
151792
|
+
async function generateViaTemporarySession(deps, input, service3) {
|
|
151793
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
151794
|
+
const base2 = service3.url.replace(/\/+$/, "");
|
|
151795
|
+
const auth = authHeaders3(service3);
|
|
151796
|
+
const dirHeader = input.directory ? { "x-opencode-directory": encodeURIComponent(input.directory) } : {};
|
|
151797
|
+
const timeout3 = deps.timeoutMs ?? 15e3;
|
|
151798
|
+
const created = await doFetch(`${base2}/api/session`, {
|
|
151799
|
+
method: "POST",
|
|
151800
|
+
headers: { "content-type": "application/json", ...auth, ...dirHeader },
|
|
151801
|
+
body: JSON.stringify({
|
|
151802
|
+
title: TEMP_SESSION_TITLE,
|
|
151803
|
+
...input.model ? { model: input.model } : {},
|
|
151804
|
+
...input.directory ? { location: { directory: input.directory } } : {}
|
|
151805
|
+
}),
|
|
151806
|
+
signal: AbortSignal.timeout(timeout3)
|
|
151807
|
+
});
|
|
151808
|
+
if (!created.ok) {
|
|
151809
|
+
const detail = await created.text().catch(() => "");
|
|
151810
|
+
throw new Error(`\u4E34\u65F6\u4F1A\u8BDD\u521B\u5EFA\u5931\u8D25 HTTP ${created.status}${detail ? `: ${detail.slice(0, 160)}` : ""}`);
|
|
151811
|
+
}
|
|
151812
|
+
const tempID = extractSessionID(await created.json());
|
|
151813
|
+
if (!tempID) throw new Error("\u4E34\u65F6\u4F1A\u8BDD\u521B\u5EFA\u54CD\u5E94\u7F3A\u5C11\u4F1A\u8BDD id");
|
|
151814
|
+
try {
|
|
151815
|
+
const generated = await doFetch(`${base2}/api/session/${encodeURIComponent(tempID)}/generate`, {
|
|
151816
|
+
method: "POST",
|
|
151817
|
+
headers: { "content-type": "application/json", ...auth, ...dirHeader },
|
|
151818
|
+
body: JSON.stringify({ prompt: input.prompt }),
|
|
151819
|
+
signal: AbortSignal.timeout(timeout3)
|
|
151820
|
+
});
|
|
151821
|
+
if (!generated.ok) {
|
|
151822
|
+
const detail = await generated.text().catch(() => "");
|
|
151823
|
+
throw new Error(`\u4E34\u65F6\u4F1A\u8BDD\u751F\u6210\u5931\u8D25 HTTP ${generated.status}${detail ? `: ${detail.slice(0, 160)}` : ""}`);
|
|
151824
|
+
}
|
|
151825
|
+
return await generated.json();
|
|
151826
|
+
} finally {
|
|
151827
|
+
await doFetch(`${base2}/api/session/${encodeURIComponent(tempID)}`, {
|
|
151828
|
+
method: "DELETE",
|
|
151829
|
+
headers: { ...auth, ...dirHeader },
|
|
151830
|
+
signal: AbortSignal.timeout(5e3)
|
|
151831
|
+
}).catch(() => void 0);
|
|
151832
|
+
}
|
|
151833
|
+
}
|
|
151834
|
+
function extractSessionID(raw) {
|
|
151835
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
151836
|
+
const record2 = raw;
|
|
151837
|
+
const data = record2["data"];
|
|
151838
|
+
const fromData = typeof data === "object" && data !== null ? data["id"] : void 0;
|
|
151839
|
+
const id2 = typeof record2["id"] === "string" ? record2["id"] : fromData;
|
|
151840
|
+
return typeof id2 === "string" && id2.startsWith("ses") ? id2 : void 0;
|
|
151841
|
+
}
|
|
151751
151842
|
function withAttemptTimeout(promise3, ms) {
|
|
151752
151843
|
return new Promise((resolve4, reject) => {
|
|
151753
151844
|
const timer = setTimeout(() => reject(new Error("generate-attempt-timeout")), Math.max(1, ms));
|
|
@@ -152010,6 +152101,12 @@ async function start(ctx, config, log2, logSink) {
|
|
|
152010
152101
|
return void 0;
|
|
152011
152102
|
}
|
|
152012
152103
|
}
|
|
152104
|
+
async function resolveGenerateModel(sessionID, directory) {
|
|
152105
|
+
const sessionModel = await readSessionModelQuiet(sessionID, directory);
|
|
152106
|
+
if (sessionModel) return { providerID: sessionModel.providerID, id: sessionModel.id };
|
|
152107
|
+
const first = (await listModels())[0];
|
|
152108
|
+
return first ? { providerID: first.providerID, id: first.id } : void 0;
|
|
152109
|
+
}
|
|
152013
152110
|
async function switchSessionModel(sessionID, model) {
|
|
152014
152111
|
const link2 = await sessionMap.resolveBySession(sessionID);
|
|
152015
152112
|
const options2 = link2?.dir ? { headers: { "x-opencode-directory": link2.dir } } : void 0;
|
|
@@ -152112,14 +152209,20 @@ async function start(ctx, config, log2, logSink) {
|
|
|
152112
152209
|
const readMessages = async (sessionID, directory) => readSessionMessages(ctx, sessionID, directory, log2);
|
|
152113
152210
|
const generateText = async (prompt, directory) => {
|
|
152114
152211
|
const api = ctx.generate.text;
|
|
152212
|
+
const model = await resolveGenerateModel(input.sessionID, directory);
|
|
152115
152213
|
const outcome = await quickGenerateWithSession(
|
|
152116
152214
|
{
|
|
152117
152215
|
log: log2,
|
|
152118
152216
|
...typeof api === "function" ? {
|
|
152119
|
-
generateText: (p, requestOptions) => api({ prompt: p }, requestOptions)
|
|
152217
|
+
generateText: (p, requestOptions, modelRef) => api({ prompt: p, ...modelRef ? { model: modelRef } : {} }, requestOptions)
|
|
152120
152218
|
} : {}
|
|
152121
152219
|
},
|
|
152122
|
-
{
|
|
152220
|
+
{
|
|
152221
|
+
prompt,
|
|
152222
|
+
sessionID: input.sessionID,
|
|
152223
|
+
...directory ? { directory } : {},
|
|
152224
|
+
...model ? { model } : {}
|
|
152225
|
+
}
|
|
152123
152226
|
);
|
|
152124
152227
|
return outcome.result;
|
|
152125
152228
|
};
|
|
@@ -152382,27 +152485,6 @@ async function start(ctx, config, log2, logSink) {
|
|
|
152382
152485
|
log2.info("\u673A\u5668\u4EBA\u83DC\u5355\u70B9\u51FB \u2192 \u6267\u884C\u547D\u4EE4", { eventKey: click.eventKey, cmd: entry.cmd, label: entry.label });
|
|
152383
152486
|
await handleMessage(synthetic);
|
|
152384
152487
|
}
|
|
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
|
|
152404
|
-
};
|
|
152405
|
-
}
|
|
152406
152488
|
async function handleQuickNew(message) {
|
|
152407
152489
|
const hint = async () => {
|
|
152408
152490
|
const res = await sender.sendCard(message.chatId, buildConsoleHintCard());
|
|
@@ -152424,200 +152506,93 @@ async function start(ctx, config, log2, logSink) {
|
|
|
152424
152506
|
} catch (err2) {
|
|
152425
152507
|
log2.debug("quick-new \u4F1A\u8BDD\u5217\u8868\u8BFB\u53D6\u5931\u8D25", { error: errorMessage(err2) });
|
|
152426
152508
|
}
|
|
152427
|
-
for (const
|
|
152428
|
-
if (
|
|
152429
|
-
seen.add(
|
|
152430
|
-
candidates.push({ path:
|
|
152509
|
+
for (const entry of entries3) {
|
|
152510
|
+
if (entry.directory && !seen.has(entry.directory)) {
|
|
152511
|
+
seen.add(entry.directory);
|
|
152512
|
+
candidates.push({ path: entry.directory, ...entry.title ? { label: entry.title } : {} });
|
|
152431
152513
|
}
|
|
152432
152514
|
}
|
|
152433
152515
|
const routingSessionID = entries3[0]?.sessionID;
|
|
152434
|
-
if (
|
|
152435
|
-
log2.debug("quick-new \u65E0\
|
|
152436
|
-
candidates: candidates.length,
|
|
152437
|
-
hasRouting: Boolean(routingSessionID)
|
|
152438
|
-
});
|
|
152516
|
+
if (!routingSessionID) {
|
|
152517
|
+
log2.debug("quick-new \u65E0\u53EF\u7528\u4F1A\u8BDD\uFF08\u751F\u6210\u901A\u9053\u7F3A\u5C11\u8DEF\u7531 sessionID\uFF09\uFF0C\u56DE\u9000\u63D0\u793A\u5361");
|
|
152439
152518
|
await hint();
|
|
152440
152519
|
return;
|
|
152441
152520
|
}
|
|
152521
|
+
let models = [];
|
|
152522
|
+
try {
|
|
152523
|
+
models = (await listModels()).map((m) => ({
|
|
152524
|
+
providerID: m.providerID,
|
|
152525
|
+
id: m.id,
|
|
152526
|
+
...m.name ? { name: m.name } : {}
|
|
152527
|
+
}));
|
|
152528
|
+
} catch (err2) {
|
|
152529
|
+
log2.debug("quick-new \u6A21\u578B\u5217\u8868\u8BFB\u53D6\u5931\u8D25", { error: errorMessage(err2) });
|
|
152530
|
+
}
|
|
152442
152531
|
const ack = await sender.sendCard(message.chatId, buildQuickNewThinkingCard());
|
|
152443
152532
|
if (!ack.ok || !ack.messageId) {
|
|
152444
152533
|
await hint();
|
|
152445
152534
|
return;
|
|
152446
152535
|
}
|
|
152447
|
-
const
|
|
152536
|
+
const genModel = await resolveGenerateModel(routingSessionID, entries3[0]?.directory);
|
|
152537
|
+
const generateText = async (prompt, requestOptions, model2) => {
|
|
152448
152538
|
const api = ctx.generate.text;
|
|
152449
152539
|
if (typeof api !== "function") throw new Error("generate.text unavailable");
|
|
152450
|
-
return api({ prompt }, requestOptions);
|
|
152540
|
+
return api({ prompt, ...model2 ? { model: model2 } : {} }, requestOptions);
|
|
152451
152541
|
};
|
|
152452
152542
|
const outcome = await quickGenerateWithSession(
|
|
152453
152543
|
{ log: log2, generateText },
|
|
152454
|
-
{
|
|
152544
|
+
{
|
|
152545
|
+
prompt: buildQuickNewPrompt({ text: message.text, candidates, models }),
|
|
152546
|
+
sessionID: routingSessionID,
|
|
152547
|
+
...genModel ? { model: genModel } : {}
|
|
152548
|
+
}
|
|
152455
152549
|
);
|
|
152456
152550
|
const decision = parseQuickNewDecision(extractGeneratedText(outcome.result));
|
|
152457
|
-
|
|
152458
|
-
|
|
152459
|
-
|
|
152460
|
-
|
|
152461
|
-
|
|
152462
|
-
|
|
152463
|
-
|
|
152464
|
-
|
|
152551
|
+
log2.debug("quick-new \u8BC6\u522B\u7ED3\u679C", {
|
|
152552
|
+
intent: decision?.intent,
|
|
152553
|
+
dir: decision?.directory,
|
|
152554
|
+
perm: decision?.perm,
|
|
152555
|
+
model: decision?.model,
|
|
152556
|
+
reason: decision?.reason
|
|
152557
|
+
});
|
|
152558
|
+
if (!decision || decision.intent === "chat") {
|
|
152465
152559
|
await sender.patchCard(ack.messageId, buildConsoleHintCard());
|
|
152466
152560
|
return;
|
|
152467
152561
|
}
|
|
152468
|
-
|
|
152469
|
-
|
|
152470
|
-
|
|
152471
|
-
|
|
152472
|
-
|
|
152473
|
-
|
|
152474
|
-
|
|
152475
|
-
|
|
152476
|
-
|
|
152477
|
-
|
|
152562
|
+
if (decision.intent === "list") {
|
|
152563
|
+
const card = await commands.buildSessionListCard(message.chatId);
|
|
152564
|
+
await sender.patchCard(ack.messageId, card);
|
|
152565
|
+
log2.info("quick-new\uFF1A\u8BC6\u522B\u5361\u5DF2\u5C31\u5730\u5207\u6362\u4E3A\u4F1A\u8BDD\u5217\u8868\u5361", { chatId: message.chatId });
|
|
152566
|
+
return;
|
|
152567
|
+
}
|
|
152568
|
+
const dir = matchCandidateDirectory(decision.directory, candidates);
|
|
152569
|
+
const validation = dir ? validateDir(dir) : void 0;
|
|
152570
|
+
const model = matchModelOption(decision.model, models);
|
|
152571
|
+
const prefill = {
|
|
152572
|
+
title: decision.title || topicTitle(message.text),
|
|
152573
|
+
...validation?.ok ? { dir: validation.path } : {},
|
|
152574
|
+
...model ? {
|
|
152575
|
+
model: {
|
|
152576
|
+
providerID: model.providerID,
|
|
152577
|
+
id: model.id,
|
|
152578
|
+
...model.name ? { name: model.name } : {}
|
|
152579
|
+
}
|
|
152580
|
+
} : {},
|
|
152581
|
+
...decision.perm ? { perm: decision.perm } : {}
|
|
152478
152582
|
};
|
|
152479
|
-
await
|
|
152480
|
-
|
|
152481
|
-
|
|
152482
|
-
|
|
152483
|
-
|
|
152484
|
-
|
|
152485
|
-
|
|
152486
|
-
|
|
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 });
|
|
152583
|
+
const form = await commands.buildPrefilledSetupForm(message.chatId, ack.messageId, prefill);
|
|
152584
|
+
await sender.patchCard(ack.messageId, form);
|
|
152585
|
+
log2.info("quick-new\uFF1AAI \u9884\u586B\u5EFA\u4F1A\u8BDD\u8868\u5355\u5DF2\u53D1\u9001\uFF08\u5C31\u5730\uFF09", {
|
|
152586
|
+
title: prefill.title,
|
|
152587
|
+
dir: validation?.ok ? validation.path : void 0,
|
|
152588
|
+
perm: decision.perm,
|
|
152589
|
+
model: model ? `${model.providerID}/${model.id}` : void 0
|
|
152590
|
+
});
|
|
152494
152591
|
} catch (err2) {
|
|
152495
152592
|
log2.warn("quick-new \u5904\u7406\u5931\u8D25\uFF0C\u56DE\u9000\u7BA1\u7406\u53F0\u63D0\u793A", { error: errorMessage(err2) });
|
|
152496
152593
|
await hint();
|
|
152497
152594
|
}
|
|
152498
152595
|
}
|
|
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
152596
|
async function runInActiveSession(message) {
|
|
152622
152597
|
let active = await sessionMap.getActive(message.chatId);
|
|
152623
152598
|
if (!active) {
|
|
@@ -152634,7 +152609,7 @@ async function start(ctx, config, log2, logSink) {
|
|
|
152634
152609
|
await runInSession(message, active.sessionID);
|
|
152635
152610
|
}
|
|
152636
152611
|
async function runInSession(message, sessionID, replyToMessageId, forceDelivery) {
|
|
152637
|
-
const delivery = forceDelivery ?? decideDelivery(executions.isRunning(sessionID));
|
|
152612
|
+
const delivery = forceDelivery ?? decideDelivery(executions.isRunning(sessionID), config.busyDelivery);
|
|
152638
152613
|
const link2 = await sessionMap.resolveBySession(sessionID);
|
|
152639
152614
|
const modelRef = await readSessionModelQuiet(sessionID, link2?.dir) ?? link2?.model;
|
|
152640
152615
|
const model = modelRef ? modelLabel(modelRef) : void 0;
|
|
@@ -152692,11 +152667,7 @@ ${downloadedAttachmentPrompt(message.attachment, outcome)}`;
|
|
|
152692
152667
|
handleForm: (a) => formRelay.handleCardAction(a),
|
|
152693
152668
|
handleStop: (a) => stop.handleCardAction(a),
|
|
152694
152669
|
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
|
-
},
|
|
152670
|
+
handleCommands: (a) => commands.handleCardAction(a),
|
|
152700
152671
|
handleApprovals: (a) => approvals.handleCardAction(a)
|
|
152701
152672
|
})
|
|
152702
152673
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-feishu-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "Feishu/Lark plugin for OpenCode V2 — single-user topics as sessions, in-card permission approvals, streaming cards. Minimal permissions, no public endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|