u1s1-cli 0.16.0 → 0.16.1
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/index.js +3 -0
- package/dist/loop.d.ts +2 -0
- package/dist/loop.js +134 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { writeFileSync } from "node:fs";
|
|
|
4
4
|
import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, 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
|
+
import { registerLoopCommand } from "./loop.js";
|
|
7
8
|
import { ensureSearchTools } from "./search-tools.js";
|
|
8
9
|
import { ensureUsableShell } from "./shell-doctor.js";
|
|
9
10
|
import { applyBrandUi, setUpdateNotice } from "./style.js";
|
|
@@ -215,6 +216,8 @@ async function runAgent(cfg, args) {
|
|
|
215
216
|
factory: (pi) => {
|
|
216
217
|
applyBrandUi(pi, VERSION);
|
|
217
218
|
offerStarterTemplates(pi);
|
|
219
|
+
// /loop: 会话级定时循环执行提示词(对标 Claude Code),实现见 loop.ts
|
|
220
|
+
registerLoopCommand(pi);
|
|
218
221
|
// 会话标题:拿用户第一条正经输入的前半截当名字(像 Claude Code 那样),
|
|
219
222
|
// /resume 的会话列表里就能一眼认出每个会话。/clear 后 session_start
|
|
220
223
|
// 会重置标记,新会话重新起标题。
|
package/dist/loop.d.ts
ADDED
package/dist/loop.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /loop —— 会话级定时循环执行提示词(对标 Claude Code 的 /loop)。
|
|
3
|
+
*
|
|
4
|
+
* 用法:
|
|
5
|
+
* /loop 5m 检查部署状态 每 5 分钟跑一次
|
|
6
|
+
* /loop 检查构建 every 2h 间隔放后面也行
|
|
7
|
+
* /loop 盯一下 PR 不写间隔,默认 10 分钟
|
|
8
|
+
* /loop 20m /review-pr 1234 循环里也能跑其他斜杠命令
|
|
9
|
+
* /loop stop 停止当前循环
|
|
10
|
+
*
|
|
11
|
+
* 语义与 Claude Code 对齐:任务只活在当前会话,/clear 或退出即消失。
|
|
12
|
+
* 实现核心是 pi.sendUserMessage():定时器到点就"替用户打字"发一轮,
|
|
13
|
+
* deliverAs: "followUp" 保证上一轮没跑完时排队等它结束,不硬插打断;
|
|
14
|
+
* expandPromptTemplates: true 让 prompt 里的斜杠命令也能被展开执行。
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_INTERVAL_MS = 10 * 60_000;
|
|
17
|
+
const STATUS_KEY = "loop";
|
|
18
|
+
/** 状态栏/通知里提示词最多显示这么长,防止长 prompt 把 UI 撑爆 */
|
|
19
|
+
const PROMPT_PREVIEW_MAX = 40;
|
|
20
|
+
let timer;
|
|
21
|
+
let activePrompt = "";
|
|
22
|
+
let activeIntervalMs = DEFAULT_INTERVAL_MS;
|
|
23
|
+
/** "30s"/"5m"/"2h"/"1d" → 毫秒;不合法返回 undefined */
|
|
24
|
+
function parseIntervalMs(token) {
|
|
25
|
+
const m = /^(\d+)([smhd])$/i.exec(token);
|
|
26
|
+
if (!m)
|
|
27
|
+
return undefined;
|
|
28
|
+
const n = Number(m[1]);
|
|
29
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
const unitMs = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2].toLowerCase()];
|
|
32
|
+
return n * unitMs;
|
|
33
|
+
}
|
|
34
|
+
function formatInterval(ms) {
|
|
35
|
+
if (ms >= 3_600_000 && ms % 3_600_000 === 0) {
|
|
36
|
+
return `${ms / 3_600_000}小时`;
|
|
37
|
+
}
|
|
38
|
+
if (ms % 60_000 === 0) {
|
|
39
|
+
return `${ms / 60_000}分钟`;
|
|
40
|
+
}
|
|
41
|
+
return `${Math.round(ms / 1_000)}秒`;
|
|
42
|
+
}
|
|
43
|
+
function preview(text) {
|
|
44
|
+
return text.length > PROMPT_PREVIEW_MAX ? `${text.slice(0, PROMPT_PREVIEW_MAX)}…` : text;
|
|
45
|
+
}
|
|
46
|
+
function stopLoop(ui) {
|
|
47
|
+
if (timer)
|
|
48
|
+
clearInterval(timer);
|
|
49
|
+
timer = undefined;
|
|
50
|
+
activePrompt = "";
|
|
51
|
+
ui?.setStatus(STATUS_KEY, undefined);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 解析出 [间隔?, 提示词]。间隔可放在最前或最后(Claude Code 同款规则),
|
|
55
|
+
* 没写就用默认 10 分钟。解析失败返回 null 并已通过 notify 报错。
|
|
56
|
+
*/
|
|
57
|
+
function parseArgs(ui, raw) {
|
|
58
|
+
const tokens = raw.trim().split(/\s+/).filter(Boolean);
|
|
59
|
+
let intervalMs;
|
|
60
|
+
let intervalIdx = -1;
|
|
61
|
+
// 末尾的 "every 30m" 形式占两个 token,先于前置匹配检查
|
|
62
|
+
if (tokens.length >= 2 && tokens[tokens.length - 2].toLowerCase() === "every") {
|
|
63
|
+
const parsed = parseIntervalMs(tokens[tokens.length - 1]);
|
|
64
|
+
if (parsed !== undefined) {
|
|
65
|
+
intervalMs = parsed;
|
|
66
|
+
intervalIdx = tokens.length - 2;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (intervalMs === undefined) {
|
|
70
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
71
|
+
const parsed = parseIntervalMs(tokens[i]);
|
|
72
|
+
if (parsed !== undefined) {
|
|
73
|
+
intervalMs = parsed;
|
|
74
|
+
intervalIdx = i;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const rest = tokens.slice();
|
|
80
|
+
if (intervalMs !== undefined) {
|
|
81
|
+
rest.splice(intervalIdx, intervalIdx === tokens.length - 2 ? 2 : 1);
|
|
82
|
+
}
|
|
83
|
+
const prompt = rest.join(" ").trim();
|
|
84
|
+
if (!prompt) {
|
|
85
|
+
ui.notify("用法: /loop [间隔] <提示词>,如 /loop 5m 检查部署状态;停止用 /loop stop", "error");
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return { intervalMs: intervalMs ?? DEFAULT_INTERVAL_MS, prompt };
|
|
89
|
+
}
|
|
90
|
+
export function registerLoopCommand(pi) {
|
|
91
|
+
// 会话重置(/clear、新会话)时循环随之消亡——和 Claude Code 的会话级语义一致
|
|
92
|
+
pi.on("session_start", (_event, ctx) => stopLoop(ctx.ui));
|
|
93
|
+
pi.registerCommand("loop", {
|
|
94
|
+
description: "定时重复执行提示词,如 /loop 5m 检查部署状态;/loop stop 停止",
|
|
95
|
+
handler: async (args, ctx) => {
|
|
96
|
+
const input = args.trim();
|
|
97
|
+
if (!input) {
|
|
98
|
+
ctx.ui.notify(timer
|
|
99
|
+
? `🔁 当前循环:每${formatInterval(activeIntervalMs)} · ${preview(activePrompt)}(/loop stop 停止)`
|
|
100
|
+
: "用法: /loop [间隔] <提示词>,如 /loop 5m 检查部署状态", "info");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (/^(stop|off|取消|停止)$/i.test(input)) {
|
|
104
|
+
if (!timer) {
|
|
105
|
+
ctx.ui.notify("没有正在运行的循环", "info");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
stopLoop(ctx.ui);
|
|
109
|
+
ctx.ui.notify("⏹ 循环已停止", "info");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const parsed = parseArgs(ctx.ui, input);
|
|
113
|
+
if (!parsed)
|
|
114
|
+
return;
|
|
115
|
+
// 已有循环在跑就直接替换成新的(先清旧定时器)
|
|
116
|
+
stopLoop(ctx.ui);
|
|
117
|
+
activePrompt = parsed.prompt;
|
|
118
|
+
activeIntervalMs = parsed.intervalMs;
|
|
119
|
+
timer = setInterval(() => {
|
|
120
|
+
try {
|
|
121
|
+
pi.sendUserMessage(activePrompt, {
|
|
122
|
+
deliverAs: "followUp",
|
|
123
|
+
expandPromptTemplates: true,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
ctx.ui.notify(`loop 执行失败: ${e instanceof Error ? e.message : String(e)}`, "error");
|
|
128
|
+
}
|
|
129
|
+
}, activeIntervalMs);
|
|
130
|
+
ctx.ui.setStatus(STATUS_KEY, `🔁 每${formatInterval(activeIntervalMs)} · ${preview(activePrompt)}`);
|
|
131
|
+
ctx.ui.notify(`🔁 已开始:每${formatInterval(activeIntervalMs)}执行一次「${preview(activePrompt)}」`, "info");
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|