u1s1-cli 0.16.6 → 0.18.0

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/dist/brand.d.ts CHANGED
@@ -9,5 +9,8 @@ export declare const HERO_ART: string[];
9
9
  * Startup hero, responsive to terminal width:
10
10
  * wide → wordmark with info column beside it; medium → stacked; narrow → one-liner.
11
11
  */
12
- export declare function renderBrandHeader(theme: Theme, version: string, cwd: string, width: number, notice?: string): string[];
12
+ export declare function renderBrandHeader(theme: Theme, version: string, cwd: string, width: number, notice?: string, announcement?: {
13
+ text: string;
14
+ url?: string;
15
+ }): string[];
13
16
  export declare function printConsoleBanner(version: string): void;
package/dist/brand.js CHANGED
@@ -42,23 +42,34 @@ function paintArt(theme, line) {
42
42
  flush();
43
43
  return out;
44
44
  }
45
+ /** 公告行:📢 前缀 + 正文(accent 色),可选链接(dim)。 */
46
+ function announcementLine(theme, a) {
47
+ const link = a.url ? ` ${theme.fg("dim", a.url)}` : "";
48
+ return `${theme.fg("accent", "📢")} ${theme.fg("accent", a.text)}${link}`;
49
+ }
45
50
  /**
46
51
  * Startup hero, responsive to terminal width:
47
52
  * wide → wordmark with info column beside it; medium → stacked; narrow → one-liner.
48
53
  */
49
- export function renderBrandHeader(theme, version, cwd, width, notice) {
54
+ export function renderBrandHeader(theme, version, cwd, width, notice, announcement) {
50
55
  const name = theme.bold(theme.fg("text", `${BRAND_NAME} v${version}`)) +
51
56
  (notice ? ` ${theme.fg("accent", notice)}` : "");
52
57
  const brand = theme.fg("muted", `${BRAND_CN} · ${BRAND_TAGLINE}`);
53
58
  const dir = theme.fg("dim", `cwd: ${formatHomePath(cwd)}`);
54
59
  const hints = theme.fg("dim", "/help 看命令 · Shift+Enter 换行 · Esc 中断");
55
60
  if (width < ART_WIDTH + 4) {
56
- return ["", ` ${theme.fg("accent", "✻")} ${name} ${theme.fg("muted", BRAND_CN)}`, ` ${dir}`, ""];
61
+ const lines = ["", ` ${theme.fg("accent", "✻")} ${name} ${theme.fg("muted", BRAND_CN)}`, ` ${dir}`];
62
+ if (announcement)
63
+ lines.push(` ${announcementLine(theme, announcement)}`);
64
+ return [...lines, ""];
57
65
  }
58
66
  const art = HERO_ART.map((line) => ` ${paintArt(theme, line)}`);
59
67
  // widest info row (hints) needs 46 cols beside the 28-col wordmark
60
68
  if (width < ART_WIDTH + 46) {
61
- return ["", ...art, "", ` ${name} ${brand}`, ` ${dir}`, ""];
69
+ const lines = ["", ...art, "", ` ${name} ${brand}`, ` ${dir}`];
70
+ if (announcement)
71
+ lines.push(` ${announcementLine(theme, announcement)}`);
72
+ return [...lines, ""];
62
73
  }
63
74
  const rows = [...art];
64
75
  const gap = " ";
@@ -66,6 +77,9 @@ export function renderBrandHeader(theme, version, cwd, width, notice) {
66
77
  rows[2] += `${gap}${brand}`;
67
78
  rows[3] += `${gap}${dir}`;
68
79
  rows[4] += `${gap}${hints}`;
80
+ // 公告放信息列末尾(最后一行字模旁),够醒目又不挤掉常规信息
81
+ if (announcement)
82
+ rows[5] += `${gap}${announcementLine(theme, announcement)}`;
69
83
  return ["", ...rows, ""];
70
84
  }
71
85
  export function printConsoleBanner(version) {
package/dist/config.js CHANGED
@@ -51,7 +51,7 @@ export function apiModelToDef(m) {
51
51
  }
52
52
  export const MODELS = [
53
53
  {
54
- id: "deepseek/deepseek-v4-flash",
54
+ id: "deepseek-v4-flash",
55
55
  name: "DeepSeek V4 Flash (u1s1)",
56
56
  aliases: ["deepseek", "flash", "v4-flash"],
57
57
  reasoning: false,
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync, spawnSync } from "node:child_process";
3
3
  import { writeFileSync } from "node:fs";
4
- import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, scrubForeignProviderEnv, toProviderModels, writeCompactUiExtension, writeWebToolsExtension, } from "./agent-setup.js";
4
+ import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, ensureWorkflowPromptTemplate, scrubForeignProviderEnv, toProviderModels, writeCompactUiExtension, writeWebToolsExtension, } from "./agent-setup.js";
5
5
  import { printConsoleBanner } from "./brand.js";
6
6
  import { agentDir, apiModelToDef, CUSTOM_ENDPOINTS, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, refValid, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
7
7
  import { registerLoopCommand } from "./loop.js";
8
8
  import { ensureSearchTools } from "./search-tools.js";
9
9
  import { ensureUsableShell } from "./shell-doctor.js";
10
- import { applyBrandUi, setUpdateNotice } from "./style.js";
10
+ import { applyBrandUi, setAnnouncement, setUpdateNotice } from "./style.js";
11
11
  import { fetchModels, loadCustomEndpoints } from "./api.js";
12
12
  const PACKAGE_NAME = "u1s1-cli";
13
13
  /** 启动时检测到的可自动安装的新版;TUI 退出后才装(见 installPendingUpdate)。 */
@@ -151,11 +151,14 @@ async function runAgent(cfg, args) {
151
151
  let imageGenEnabled = false;
152
152
  const endpointsReady = loadCustomEndpoints(cfg);
153
153
  try {
154
- const { models, features } = await fetchModels(cfg);
154
+ const { models, features, announcement } = await fetchModels(cfg);
155
155
  setModelsFromApi(models.map(apiModelToDef));
156
156
  webSearchEnabled = features.web_search !== false;
157
157
  webFetchRenderEnabled = features.web_fetch_render === true;
158
158
  imageGenEnabled = features.image_gen === true;
159
+ // 后台下发的启动公告(与模型列表同一次请求捎回,零额外开销);空/老网关不显示
160
+ if (announcement?.text)
161
+ setAnnouncement(announcement);
159
162
  }
160
163
  catch (e) {
161
164
  console.error(" 获取模型列表失败,使用内置列表:", e.message);
@@ -163,6 +166,7 @@ async function runAgent(cfg, args) {
163
166
  await endpointsReady;
164
167
  ensureBrandPrompt(await shellReady);
165
168
  ensureProviderModels(cfg);
169
+ ensureWorkflowPromptTemplate();
166
170
  // 联网工具经 agentDir/extensions 投影,TUI 和 u1s1 web 共用一份注册
167
171
  writeWebToolsExtension(cfg, {
168
172
  webSearch: webSearchEnabled,
package/dist/style.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export declare function setAnnouncement(next: {
3
+ text: string;
4
+ url?: string;
5
+ } | undefined): void;
2
6
  export declare function setUpdateNotice(notice: string): void;
3
7
  /**
4
8
  * Brand chrome is just the startup hero + window title; everything else
package/dist/style.js CHANGED
@@ -8,6 +8,12 @@ import { renderBrandHeader } from "./brand.js";
8
8
  */
9
9
  let updateNotice = "";
10
10
  let refreshHeader;
11
+ /** 启动横幅上的运营公告(后台下发,见 fetchModels 的 announcement 字段)。 */
12
+ let announcement;
13
+ export function setAnnouncement(next) {
14
+ announcement = next;
15
+ refreshHeader?.();
16
+ }
11
17
  export function setUpdateNotice(notice) {
12
18
  updateNotice = notice;
13
19
  refreshHeader?.();
@@ -28,7 +34,7 @@ export function applyBrandUi(pi, version) {
28
34
  return {
29
35
  render(width) {
30
36
  // pi-tui crashes on lines wider than the terminal, so truncate defensively.
31
- return renderBrandHeader(theme, version, process.cwd(), width, updateNotice).map((line) => truncateToWidth(line, width));
37
+ return renderBrandHeader(theme, version, process.cwd(), width, updateNotice, announcement).map((line) => truncateToWidth(line, width));
32
38
  },
33
39
  invalidate() { },
34
40
  };
@@ -0,0 +1,44 @@
1
+ /** 同时在跑的子 agent 上限;超额排队,防止打爆网关。 */
2
+ export declare const SUBAGENT_CONCURRENCY = 4;
3
+ /** 单个子任务默认超时;到点 abort,不让一个卡死的任务拖住整批。 */
4
+ export declare const SUBAGENT_TIMEOUT_MS: number;
5
+ /** 主会话当前模型的引用,用于子 agent 默认继承同款模型。 */
6
+ export type ParentModelRef = {
7
+ provider: string;
8
+ id: string;
9
+ } | undefined;
10
+ export interface SubagentOptions {
11
+ task: string;
12
+ /** 主会话当前模型(缺省时子 agent 尝试继承它)。 */
13
+ parentModel?: ParentModelRef;
14
+ /** 覆盖模型:"provider/id" 或裸 id。 */
15
+ model?: string;
16
+ timeoutMs?: number;
17
+ signal?: AbortSignal;
18
+ /** 禁用全部工具(纯文本推理,判分等廉价场景用)。 */
19
+ noTools?: boolean;
20
+ /** 子 agent 的工作目录(git worktree 隔离等场景);缺省继承主进程 cwd。 */
21
+ cwd?: string;
22
+ }
23
+ /** 单个子 agent 的 token/费用用量(汇总自会话内全部 assistant 消息)。 */
24
+ export interface SubagentUsage {
25
+ totalTokens: number;
26
+ costUsd: number;
27
+ }
28
+ export interface SubagentOutcome {
29
+ ok: boolean;
30
+ text: string;
31
+ usage: SubagentUsage;
32
+ }
33
+ /**
34
+ * Spawn 一个独立上下文的子 agent 执行任务,返回其最终文本输出。
35
+ * 模型解析:"provider/id" 精确匹配;裸 id(如 deepseek-v4-flash)先按原样找,
36
+ * 再兜底到 u1s1 provider —— 和 CLI --model 的解析习惯保持一致。
37
+ * 失败(模型错误/超时/取消)抛错,由调用方决定容错语义。
38
+ */
39
+ export declare function runSubagent(opts: SubagentOptions): Promise<SubagentOutcome>;
40
+ /**
41
+ * 固定并发的工作池:limit 个工人依次领任务跑完一个补位一个。
42
+ * worker 抛错由调用方在 fn 内部兜住;signal 中止后不再领新任务。
43
+ */
44
+ export declare function runPool<T>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<void>, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,133 @@
1
+ import { createAgentSession, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, } from "@earendil-works/pi-coding-agent";
2
+ // ---- 子 agent spawn 基础设施:spawn_subagent 工具与 workflow runner 共用 ----
3
+ /** 同时在跑的子 agent 上限;超额排队,防止打爆网关。 */
4
+ export const SUBAGENT_CONCURRENCY = 4;
5
+ /** 单个子任务默认超时;到点 abort,不让一个卡死的任务拖住整批。 */
6
+ export const SUBAGENT_TIMEOUT_MS = 15 * 60_000;
7
+ /** ModelRuntime 进程内只建一次(auth/models 解析有启动开销)。 */
8
+ let sharedModelRuntime;
9
+ /** 从会话消息里取最后一条非空 assistant 文本作为子 agent 的最终输出。 */
10
+ function extractFinalText(messages) {
11
+ for (let i = messages.length - 1; i >= 0; i--) {
12
+ const content = messages[i]?.content;
13
+ if (!Array.isArray(content))
14
+ continue;
15
+ const text = content
16
+ .filter((b) => b?.type === "text")
17
+ .map((b) => String(b.text ?? ""))
18
+ .join("\n")
19
+ .trim();
20
+ if (text)
21
+ return text;
22
+ }
23
+ return "(子 agent 结束但没有文本输出)";
24
+ }
25
+ /**
26
+ * 汇总会话内全部 assistant 消息的 token 与费用。
27
+ * pi-ai 对自定义 provider(models.json)不算钱(usage.cost.total 恒 0),
28
+ * 所以优先用自带费用,否则按模型价率(美元/百万 token)自算。
29
+ */
30
+ function extractUsage(messages, model) {
31
+ let totalTokens = 0;
32
+ let costUsd = 0;
33
+ const rates = model?.cost;
34
+ for (const m of messages) {
35
+ const msg = m;
36
+ const usage = msg?.usage;
37
+ if (!usage || msg.role !== "assistant")
38
+ continue;
39
+ totalTokens += usage.totalTokens ?? 0;
40
+ const reported = usage.cost?.total ?? 0;
41
+ if (reported > 0) {
42
+ costUsd += reported;
43
+ }
44
+ else if (rates) {
45
+ costUsd +=
46
+ ((usage.input ?? 0) * rates.input +
47
+ (usage.output ?? 0) * rates.output +
48
+ (usage.cacheRead ?? 0) * rates.cacheRead +
49
+ (usage.cacheWrite ?? 0) * rates.cacheWrite) /
50
+ 1_000_000;
51
+ }
52
+ }
53
+ return { totalTokens, costUsd };
54
+ }
55
+ /**
56
+ * Spawn 一个独立上下文的子 agent 执行任务,返回其最终文本输出。
57
+ * 模型解析:"provider/id" 精确匹配;裸 id(如 deepseek-v4-flash)先按原样找,
58
+ * 再兜底到 u1s1 provider —— 和 CLI --model 的解析习惯保持一致。
59
+ * 失败(模型错误/超时/取消)抛错,由调用方决定容错语义。
60
+ */
61
+ export async function runSubagent(opts) {
62
+ sharedModelRuntime ??= await ModelRuntime.create();
63
+ let model;
64
+ if (opts.model) {
65
+ const slash = opts.model.indexOf("/");
66
+ const providerId = slash === -1 ? "u1s1" : opts.model.slice(0, slash);
67
+ const modelId = slash === -1 ? opts.model : opts.model.slice(slash + 1);
68
+ model =
69
+ sharedModelRuntime.getModel(providerId, modelId) ??
70
+ (slash === -1 ? sharedModelRuntime.getModel("u1s1", opts.model) : undefined);
71
+ if (!model)
72
+ throw new Error(`找不到模型 "${opts.model}"(可用 /model 查看已配置的模型)`);
73
+ }
74
+ else if (opts.parentModel) {
75
+ // 继承主会话模型;解析不到(端点被删等)就落回默认,不硬失败
76
+ model = sharedModelRuntime.getModel(opts.parentModel.provider, opts.parentModel.id);
77
+ }
78
+ else {
79
+ model = undefined;
80
+ }
81
+ // 子会话不加载精简 UI 扩展:它用 process.cwd() 重建内置工具,会破坏自定义
82
+ // cwd(worktree 隔离)下的路径解析;且 UI 美化对无界面的子 agent 毫无意义
83
+ const loader = new DefaultResourceLoader({
84
+ agentDir: getAgentDir(),
85
+ cwd: opts.cwd ?? process.cwd(),
86
+ extensionsOverride: (base) => ({
87
+ ...base,
88
+ extensions: base.extensions.filter((e) => !JSON.stringify(e).includes("u1s1-compact-ui")),
89
+ }),
90
+ });
91
+ await loader.reload();
92
+ const { session } = await createAgentSession({
93
+ // cwd 必须同时传给顶层(工具的路径解析基于它)和 sessionManager
94
+ ...(opts.cwd ? { cwd: opts.cwd } : {}),
95
+ resourceLoader: loader,
96
+ sessionManager: SessionManager.inMemory(opts.cwd ?? process.cwd()),
97
+ modelRuntime: sharedModelRuntime,
98
+ ...(model ? { model } : {}),
99
+ ...(opts.noTools ? { noTools: "all" } : {}),
100
+ });
101
+ const timeoutMs = opts.timeoutMs ?? SUBAGENT_TIMEOUT_MS;
102
+ const timer = setTimeout(() => void session.abort(), timeoutMs);
103
+ const onParentAbort = () => void session.abort();
104
+ opts.signal?.addEventListener("abort", onParentAbort, { once: true });
105
+ try {
106
+ await session.prompt(opts.task);
107
+ }
108
+ finally {
109
+ clearTimeout(timer);
110
+ opts.signal?.removeEventListener("abort", onParentAbort);
111
+ session.dispose();
112
+ }
113
+ if (opts.signal?.aborted)
114
+ throw new Error("已取消");
115
+ const messages = session.messages;
116
+ return { ok: true, text: extractFinalText(messages), usage: extractUsage(messages, model) };
117
+ }
118
+ /**
119
+ * 固定并发的工作池:limit 个工人依次领任务跑完一个补位一个。
120
+ * worker 抛错由调用方在 fn 内部兜住;signal 中止后不再领新任务。
121
+ */
122
+ export async function runPool(items, limit, fn, signal) {
123
+ let next = 0;
124
+ const worker = async () => {
125
+ for (;;) {
126
+ const i = next++;
127
+ if (i >= items.length || signal?.aborted)
128
+ return;
129
+ await fn(items[i], i);
130
+ }
131
+ };
132
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
133
+ }
package/dist/tools.d.ts CHANGED
@@ -1,10 +1,33 @@
1
+ import { ParentModelRef } from "./subagent.js";
2
+ import { Text } from "@earendil-works/pi-tui";
1
3
  import { Type } from "typebox";
2
4
  import type { CliConfig } from "./config.js";
5
+ export declare function truncate(text: string): string;
6
+ /** 联网工具通用渲染:调用行不占位,收起时只显一行摘要,ctrl+o 展开全文,出错显一行 ✗ */
7
+ export declare function compactResultRender(result: {
8
+ content: Array<{
9
+ type: string;
10
+ text?: string;
11
+ }>;
12
+ }, options: {
13
+ expanded: boolean;
14
+ }, theme: {
15
+ fg: (color: any, text: string) => string;
16
+ }, context: {
17
+ isError: boolean;
18
+ }, summaryLine: string): Text;
3
19
  /** 联网搜索工具:走 u1s1 网关代理,上游 key 不落到用户机器上。 */
4
20
  export declare function createSearchTool(cfg: Pick<CliConfig, "baseUrl" | "apiKey">): import("@earendil-works/pi-coding-agent").ToolDefinition<Type.TObject<{
5
21
  query: Type.TString;
6
22
  maxResults: Type.TOptional<Type.TNumber>;
7
23
  }>, unknown, any> & import("@earendil-works/pi-coding-agent").ToolDefinition<any, any, any>;
24
+ /** 子 agent 工具:独立上下文窗口干活,只把结果摘要带回主对话,不污染主 token 预算。 */
25
+ export declare function createSubagentTool(getParentModel: () => ParentModelRef): import("@earendil-works/pi-coding-agent").ToolDefinition<Type.TObject<{
26
+ task: Type.TOptional<Type.TString>;
27
+ tasks: Type.TOptional<Type.TArray<Type.TString>>;
28
+ model: Type.TOptional<Type.TString>;
29
+ timeout_minutes: Type.TOptional<Type.TNumber>;
30
+ }>, unknown, any> & import("@earendil-works/pi-coding-agent").ToolDefinition<any, any, any>;
8
31
  /** 生图工具:走 u1s1 网关代理火山方舟 Seedream,上游 key 不落到用户机器上。 */
9
32
  export declare function createImageTool(cfg: Pick<CliConfig, "baseUrl" | "apiKey">): import("@earendil-works/pi-coding-agent").ToolDefinition<Type.TObject<{
10
33
  prompt: Type.TString;
package/dist/tools.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { dirname, extname, resolve } from "node:path";
3
3
  import { defineTool } from "@earendil-works/pi-coding-agent";
4
+ import { runPool, runSubagent, SUBAGENT_CONCURRENCY, SUBAGENT_TIMEOUT_MS } from "./subagent.js";
4
5
  import { Text } from "@earendil-works/pi-tui";
5
6
  import { Type } from "typebox";
6
7
  import { generateImage, renderPage, searchWeb } from "./api.js";
@@ -32,7 +33,7 @@ function htmlToText(html) {
32
33
  .replace(/\n\s*\n\s*\n+/g, "\n\n")
33
34
  .trim();
34
35
  }
35
- function truncate(text) {
36
+ export function truncate(text) {
36
37
  if (text.length <= MAX_TEXT_CHARS)
37
38
  return text;
38
39
  return `${text.slice(0, MAX_TEXT_CHARS)}\n\n…(内容过长已截断,共 ${text.length} 字符)`;
@@ -87,7 +88,7 @@ function resultText(result) {
87
88
  return c && c.type === "text" ? (c.text ?? "") : "";
88
89
  }
89
90
  /** 联网工具通用渲染:调用行不占位,收起时只显一行摘要,ctrl+o 展开全文,出错显一行 ✗ */
90
- function compactResultRender(result, options,
91
+ export function compactResultRender(result, options,
91
92
  // 放宽成 any 只为避开 pi 的 ThemeColor 联合类型在结构化匹配时的逆变报错;
92
93
  // 实际传入的就是 pi 的 Theme,取值只用 "warning"/"muted" 两色
93
94
  theme, context, summaryLine) {
@@ -148,6 +149,101 @@ export function createSearchTool(cfg) {
148
149
  },
149
150
  });
150
151
  }
152
+ // ---- spawn_subagent 工具:spawn 设施在 ./subagent.ts,与 workflow runner 共用 ----
153
+ /** 单次调用最多并行任务数;再多模型也看不过来,让主 agent 分批调。 */
154
+ const SUBAGENT_MAX_TASKS = 10;
155
+ /** 子 agent 工具:独立上下文窗口干活,只把结果摘要带回主对话,不污染主 token 预算。 */
156
+ export function createSubagentTool(getParentModel) {
157
+ return defineTool({
158
+ name: "spawn_subagent",
159
+ label: "子任务",
160
+ description: "Spawn an independent sub-agent with its own context window to complete a self-contained task, then return its final answer. " +
161
+ "Pass `tasks` (array) to run N sub-agents concurrently — ideal for embarrassingly parallel work like translating many locale files, " +
162
+ "reviewing several files, or researching multiple directions at once. Each sub-agent can read/write files and run commands. " +
163
+ "Results come back as summaries; the main conversation never holds the sub-agents' intermediate state.",
164
+ promptSnippet: "Delegate self-contained tasks to independent sub-agents, optionally many in parallel",
165
+ promptGuidelines: [
166
+ "Use spawn_subagent when a task splits into independent pieces (e.g. one locale file per language): pass a `tasks` array and they run concurrently.",
167
+ "Each task description must be fully self-contained — sub-agents start with zero context about the conversation.",
168
+ "Each call costs tokens per sub-agent; don't fan out for trivial work a couple of tool calls could finish.",
169
+ ],
170
+ parameters: Type.Object({
171
+ task: Type.Optional(Type.String({ description: "Single self-contained task for one sub-agent." })),
172
+ tasks: Type.Optional(Type.Array(Type.String(), {
173
+ description: "Multiple self-contained tasks to run in concurrent sub-agents (up to 10). Use instead of `task` for parallelizable work.",
174
+ maxItems: SUBAGENT_MAX_TASKS,
175
+ })),
176
+ model: Type.Optional(Type.String({
177
+ description: 'Model for the sub-agent(s) as "provider/id" or bare id (defaults to the current conversation model).',
178
+ })),
179
+ timeout_minutes: Type.Optional(Type.Number({
180
+ description: `Per-task timeout in minutes (default ${SUBAGENT_TIMEOUT_MS / 60_000}, max 60). Timed-out tasks are aborted, not fatal to the batch.`,
181
+ minimum: 1,
182
+ maximum: 60,
183
+ })),
184
+ }),
185
+ // 精简展示:收起一行摘要,ctrl+o 展开完整报告
186
+ renderShell: "self",
187
+ renderCall() {
188
+ return new Text("", 0, 0);
189
+ },
190
+ renderResult(result, options, theme, context) {
191
+ const d = result.details;
192
+ const summary = d && typeof d.count === "number"
193
+ ? `🤖 子任务 ×${d.count}` +
194
+ (typeof d.ok === "number" && d.failed ? ` · ✓${d.ok} ✗${d.failed}` : "") +
195
+ (typeof d.seconds === "number" ? ` · ${d.seconds}s` : "")
196
+ : "🤖 子任务";
197
+ return compactResultRender(result, options, theme, context, summary);
198
+ },
199
+ async execute(_toolCallId, params, signal) {
200
+ const list = (params.tasks?.length ? params.tasks : params.task ? [params.task] : [])
201
+ .map((t) => t.trim())
202
+ .filter(Boolean)
203
+ .slice(0, SUBAGENT_MAX_TASKS);
204
+ if (list.length === 0) {
205
+ throw new Error("task 和 tasks 至少填一个");
206
+ }
207
+ const parentModel = getParentModel();
208
+ const timeoutMs = Math.min(60, params.timeout_minutes ?? SUBAGENT_TIMEOUT_MS / 60_000) * 60_000;
209
+ // 标记子 agent 环境:扩展生成器据此不再给孙 agent 注册本工具,杜绝递归 spawn
210
+ const prevFlag = process.env.U1S1_IN_SUBAGENT;
211
+ process.env.U1S1_IN_SUBAGENT = "1";
212
+ const started = Date.now();
213
+ const outcomes = new Array(list.length);
214
+ try {
215
+ await runPool(list, SUBAGENT_CONCURRENCY, async (task, i) => {
216
+ outcomes[i] = await runSubagent({ task, parentModel, model: params.model, timeoutMs, signal }).catch((e) => ({ ok: false, text: e.message, usage: { totalTokens: 0, costUsd: 0 } }));
217
+ }, signal);
218
+ }
219
+ finally {
220
+ if (prevFlag === undefined)
221
+ delete process.env.U1S1_IN_SUBAGENT;
222
+ else
223
+ process.env.U1S1_IN_SUBAGENT = prevFlag;
224
+ }
225
+ const seconds = Math.round((Date.now() - started) / 1000);
226
+ const failedIdx = outcomes.map((o, i) => (o?.ok ? -1 : i)).filter((i) => i >= 0);
227
+ const details = { count: list.length, ok: list.length - failedIdx.length, failed: failedIdx.length, seconds };
228
+ // 单任务成功直接回原文,别套报告壳浪费 token
229
+ if (list.length === 1 && !failedIdx.length) {
230
+ return {
231
+ content: [{ type: "text", text: truncate(outcomes[0]?.text ?? "") }],
232
+ details: { ...details, seconds: undefined },
233
+ };
234
+ }
235
+ const lines = [`共 ${list.length} 个子任务,✓${details.ok} ✗${details.failed},用时 ${seconds}s`, ""];
236
+ outcomes.forEach((o, i) => {
237
+ lines.push(`## 任务 ${i + 1}/${list.length} ${o?.ok ? "✅" : "❌"}`, "");
238
+ lines.push(o?.ok ? o.text : `失败:${o?.text ?? "未知错误"}`, "", "---", "");
239
+ });
240
+ return {
241
+ content: [{ type: "text", text: truncate(lines.join("\n").trim()) }],
242
+ details,
243
+ };
244
+ },
245
+ });
246
+ }
151
247
  /** 生图链路 = 网关排队 + 方舟生成(2K 实测 ~10s,4K 更久)+ 下载落盘。 */
152
248
  const IMAGE_TIMEOUT_MS = 150_000;
153
249
  /** 方舟单张参考图原图上限 10MB。 */
package/dist/web.js CHANGED
@@ -2,7 +2,7 @@ import { spawn, spawnSync } from "node:child_process";
2
2
  import { mkdirSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { dirname, join } from "node:path";
5
- import { cleanupBrandThemes, endpointKeyEnv, ensureAuthCredential, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, scrubForeignProviderEnv, writeWebToolsExtension, } from "./agent-setup.js";
5
+ import { cleanupBrandThemes, endpointKeyEnv, ensureAuthCredential, ensureBrandPrompt, ensureWorkflowPromptTemplate, ensureDefaultSettings, ensureProviderModels, scrubForeignProviderEnv, writeWebToolsExtension, } from "./agent-setup.js";
6
6
  import { agentDir, apiModelToDef, resolvePreferredModel, setModelsFromApi, u1s1Dir, writeAgentDefaultModel, } from "./config.js";
7
7
  import { ensureSearchTools } from "./search-tools.js";
8
8
  import { ensureUsableShell } from "./shell-doctor.js";
@@ -47,6 +47,8 @@ export async function prepareWebEnv(cfg) {
47
47
  }
48
48
  await endpointsReady;
49
49
  ensureBrandPrompt(await shellReady);
50
+ // /workflow 提示词模板与 TUI 同源,web/桌面版也要有
51
+ ensureWorkflowPromptTemplate();
50
52
  ensureProviderModels(cfg);
51
53
  // pi-web-ui 靠 auth.json 判断「已配置」,否则网页会弹 pi 安装引导
52
54
  ensureAuthCredential();
@@ -0,0 +1,48 @@
1
+ import { type ParentModelRef } from "../subagent.js";
2
+ /** 整个 run 的默认墙钟上限。 */
3
+ export declare const WORKFLOW_TIMEOUT_MS: number;
4
+ /** 默认整个 run 的 token 预算;0 或负数表示不设限。 */
5
+ export declare const WORKFLOW_DEFAULT_BUDGET_TOKENS = 20000000;
6
+ /** 静态白名单校验:语法能编译 + 不碰沙箱之外的任何能力。 */
7
+ export declare function validateScript(code: string): string[];
8
+ export interface WorkflowRunResult {
9
+ /** 给主 agent 的完整报告(markdown)。 */
10
+ report: string;
11
+ details: {
12
+ spawns: number;
13
+ ok: number;
14
+ failed: number;
15
+ cached: number;
16
+ seconds: number;
17
+ aborted: boolean;
18
+ };
19
+ }
20
+ export interface WorkflowProgress {
21
+ spawns: number;
22
+ ok: number;
23
+ failed: number;
24
+ cached: number;
25
+ running: number;
26
+ spentTokens: number;
27
+ budgetTokens: number;
28
+ }
29
+ export interface WorkflowRunOptions {
30
+ /** 脚本源码(调用方负责落盘)。 */
31
+ code: string;
32
+ /** 进度存档路径;同名文件存在且 resume=true 时跳过已完成任务。 */
33
+ progressPath: string;
34
+ resume?: boolean;
35
+ parentModel: ParentModelRef;
36
+ signal?: AbortSignal;
37
+ timeoutMs?: number;
38
+ /** 整个 run 的 token 上限;缺省 WORKFLOW_DEFAULT_BUDGET_TOKENS,<=0 不限。 */
39
+ budgetTokens?: number;
40
+ /** 每次子任务状态变化时回调(工具层节流后转成流式进度)。 */
41
+ onProgress?: (p: WorkflowProgress) => void;
42
+ }
43
+ /** 执行一个 workflow 脚本,返回给主对话的报告。不抛错——失败也进报告让模型自行修复。 */
44
+ export declare function runWorkflow(opts: WorkflowRunOptions): Promise<WorkflowRunResult>;
45
+ /** 工作区目录:项目根 .u1s1/workflows/(脚本 + 进度存档都在这里)。 */
46
+ export declare function workflowsDir(): string;
47
+ /** 内联脚本落盘,返回脚本路径(进度存档按同名约定派生)。 */
48
+ export declare function saveWorkflowScript(code: string): string;