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