nx-kv 0.8.3 → 0.8.4
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/CHANGELOG.md +21 -0
- package/package.json +1 -1
- package/src/modules/localtask/index.js +15 -0
- package/src/modules/localtask/service.js +156 -0
- package/src/modules/localtask/view.jsx +102 -9
- package/src/runtime/api.js +5 -0
- package/src/web/public/assets/{CliHints-DNK3TaU5.js → CliHints-D70_R4vQ.js} +1 -1
- package/src/web/public/assets/{index-Iz1qH6TC.js → index-2OK9SG2y.js} +2 -2
- package/src/web/public/assets/{view-7pNoeCnZ.js → view-C5ictDJi.js} +1 -1
- package/src/web/public/assets/view-DxwcTgBZ.js +1 -0
- package/src/web/public/assets/{view-D_gCbqXF.js → view-gzBv_kjb.js} +1 -1
- package/src/web/public/assets/{view-Dy0w1I4l.js → view-l_lnGEBC.js} +1 -1
- package/src/web/public/index.html +1 -1
- package/src/web/public/assets/view-CCkuLOoM.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录对外可见的变更。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。
|
|
4
4
|
|
|
5
|
+
## [0.8.4] - 2026-10-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **主题总览加「实时连接」探活指示灯**:1.5s 数据轮询 + EventSource 双心跳,
|
|
10
|
+
主题总览标题旁显示「实时连接正常 / 连接断开,正在重连… / 正在连接…」三态
|
|
11
|
+
(断线用红色语义提示);轮询失败不再每 1.5s 弹错误 toast,统一由指示灯提示。
|
|
12
|
+
- **主题现状 / 上一轮完成总结 / 长任务文本支持点击弹窗阅读**:在原位完整展示基础上,
|
|
13
|
+
叠加点击在居中模态框中专注阅读(长任务文本长度 >50 时才显示可点击指针);
|
|
14
|
+
支持点遮罩、按 Esc 或「关闭」按钮退出。纯前端交互、无新接口。
|
|
15
|
+
- **wait 生命周期事件流(后端 SSE)**:
|
|
16
|
+
- HTTP 为 **SSE** `GET /api/localtasks/events`(可带 `cwd`/`key` 过滤,缺省订阅全部主题,
|
|
17
|
+
首帧 `: connected`、每 15s 心跳、断线自动重连);CLI 等价为
|
|
18
|
+
`localtask events [--cwd|--key]`,逐行 NDJSON。
|
|
19
|
+
- 事件三类:`waiting`(开始等待,连接时已在等待补发 `replay` 快照)、
|
|
20
|
+
`reminder`(剩余 **30s / 10s** 各提醒一次)、`resolved`(`delivered` 被回填 /
|
|
21
|
+
`timeout` 超时;对等待标志残留的僵死状态补发 `stale:true` 的 timeout)。
|
|
22
|
+
- 后端生成器 `watchEvents` 与传输 `streamEventsSSE` 解耦,单测可独立驱动生成器。
|
|
23
|
+
- `api.js` 增 `action.stream === true` 机制:透传 req/res 给 action.run,
|
|
24
|
+
由 action 自己接管响应(SSE 等长连接用得到)。
|
|
25
|
+
|
|
5
26
|
## [0.8.3] - 2026-10-12
|
|
6
27
|
|
|
7
28
|
### Changed
|
package/package.json
CHANGED
|
@@ -282,5 +282,20 @@ export default {
|
|
|
282
282
|
summary: '探测主面板上当前打开的「实时任务」标签数(活动 SSE 连接数;用于 wait 不重复开页)',
|
|
283
283
|
run: () => service.panelPresence(),
|
|
284
284
|
},
|
|
285
|
+
|
|
286
|
+
{
|
|
287
|
+
// 等待生命周期事件流:SSE 端点(带 cwd/key 过滤),由 watchEvents 派生 waiting/reminder/resolved。
|
|
288
|
+
// action 标 stream:true → api.js 透传 req/res,run 自己接管响应不走 sendJson。
|
|
289
|
+
id: 'localtask.events',
|
|
290
|
+
cli: ['localtask', 'events'],
|
|
291
|
+
http: ['GET', '/api/localtasks/events'],
|
|
292
|
+
summary: '等待事件流:waiting 开始 / reminder 30s·10s / resolved delivered|timeout(SSE 长连接)',
|
|
293
|
+
flags: {
|
|
294
|
+
cwd: { type: 'string', hint: '工作目录过滤(与 --key 二选一)' },
|
|
295
|
+
key: { type: 'string', hint: '主题 key 过滤(与 --cwd 二选一)' },
|
|
296
|
+
},
|
|
297
|
+
stream: true,
|
|
298
|
+
run: (ctx) => service.streamEventsSSE(ctx.__raw.req, ctx.__raw.res, { cwd: ctx.cwd, key: ctx.key }),
|
|
299
|
+
},
|
|
285
300
|
],
|
|
286
301
|
};
|
|
@@ -991,3 +991,159 @@ async function ensureMainPanel() {
|
|
|
991
991
|
}
|
|
992
992
|
return { running: false, started: true, url };
|
|
993
993
|
}
|
|
994
|
+
|
|
995
|
+
// ─── 等待生命周期事件流(主面板及时通知,避免用户漏过超时)────────────
|
|
996
|
+
//
|
|
997
|
+
// wait 与常驻主面板通常是两个进程,内存 EventEmitter 跨不过进程,所以事件流
|
|
998
|
+
// 不依赖进程内回调,而是轮询 store,从 waiting/waitingUntil 与「已领取数」的变化里
|
|
999
|
+
// 派生事件:waiting / reminder(剩余 30s、10s)/ resolved(delivered 或 timeout)。
|
|
1000
|
+
// HTTP(SSE) 与 CLI(NDJSON) 共用本生成器。
|
|
1001
|
+
|
|
1002
|
+
const EVENT_POLL_MS = 500;
|
|
1003
|
+
const REMINDER_SEC = [30, 10]; // 剩余 ≤30s、≤10s 各提醒一次
|
|
1004
|
+
|
|
1005
|
+
// 纯函数:给定剩余秒数与已提醒过的阈值,返回本次应触发的阈值(秒)。便于单测。
|
|
1006
|
+
export function remindersDue(remainSec, firedSet) {
|
|
1007
|
+
const out = [];
|
|
1008
|
+
for (const sec of REMINDER_SEC) {
|
|
1009
|
+
if (remainSec <= sec && remainSec > 0 && !firedSet.has(sec)) out.push(sec);
|
|
1010
|
+
}
|
|
1011
|
+
return out;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function consumedCount(t) {
|
|
1015
|
+
return (Array.isArray(t.tasks) ? t.tasks : []).filter((x) => x.status === 'consumed').length;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* 订阅等待生命周期事件(异步生成器)。
|
|
1020
|
+
* @param {{cwd?:string,key?:string,pollMs?:number,signal?:AbortSignal}} opts
|
|
1021
|
+
* 不传 cwd/key 则订阅全部主题(主面板总览需要)。
|
|
1022
|
+
*/
|
|
1023
|
+
export async function* watchEvents({ cwd: rawCwd, key: rawKey, pollMs = EVENT_POLL_MS, signal } = {}) {
|
|
1024
|
+
let onlyKey = null;
|
|
1025
|
+
if (rawKey && String(rawKey).trim()) onlyKey = String(rawKey).trim();
|
|
1026
|
+
else if (rawCwd && String(rawCwd).trim()) onlyKey = normalizeCwd(rawCwd).key;
|
|
1027
|
+
|
|
1028
|
+
const sleep = (ms) =>
|
|
1029
|
+
new Promise((res) => {
|
|
1030
|
+
const to = setTimeout(res, ms);
|
|
1031
|
+
if (signal) {
|
|
1032
|
+
signal.addEventListener(
|
|
1033
|
+
'abort',
|
|
1034
|
+
() => {
|
|
1035
|
+
clearTimeout(to);
|
|
1036
|
+
res();
|
|
1037
|
+
},
|
|
1038
|
+
{ once: true }
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
const state = new Map();
|
|
1044
|
+
let first = true;
|
|
1045
|
+
while (!(signal && signal.aborted)) {
|
|
1046
|
+
let topics = [];
|
|
1047
|
+
try {
|
|
1048
|
+
const store = await loadStore();
|
|
1049
|
+
topics = Object.values(store.topics).filter(
|
|
1050
|
+
(t) => Array.isArray(t.tasks) && (!onlyKey || t.key === onlyKey)
|
|
1051
|
+
);
|
|
1052
|
+
} catch {
|
|
1053
|
+
topics = [];
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const now = Date.now();
|
|
1057
|
+
for (const t of topics) {
|
|
1058
|
+
const deadline = parseStampMs(t.waitingUntil);
|
|
1059
|
+
const isWaiting = !!t.waiting && Number.isFinite(deadline) && deadline > now;
|
|
1060
|
+
let st = state.get(t.key);
|
|
1061
|
+
|
|
1062
|
+
if (isWaiting) {
|
|
1063
|
+
if (!st || st.phase !== 'waiting' || st.waitingUntil !== t.waitingUntil) {
|
|
1064
|
+
st = {
|
|
1065
|
+
phase: 'waiting',
|
|
1066
|
+
waitingUntil: t.waitingUntil,
|
|
1067
|
+
deadline,
|
|
1068
|
+
consumed0: consumedCount(t),
|
|
1069
|
+
reminders: new Set(),
|
|
1070
|
+
};
|
|
1071
|
+
state.set(t.key, st);
|
|
1072
|
+
yield {
|
|
1073
|
+
type: 'waiting',
|
|
1074
|
+
key: t.key,
|
|
1075
|
+
cwd: t.cwd || '',
|
|
1076
|
+
name: t.name || '',
|
|
1077
|
+
summary: t.summary || '',
|
|
1078
|
+
waitingUntil: t.waitingUntil,
|
|
1079
|
+
remainMs: Math.max(0, deadline - now),
|
|
1080
|
+
replay: first,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
const remainSec = Math.floor((st.deadline - now) / 1000);
|
|
1084
|
+
for (const sec of remindersDue(remainSec, st.reminders)) {
|
|
1085
|
+
st.reminders.add(sec);
|
|
1086
|
+
yield {
|
|
1087
|
+
type: 'reminder',
|
|
1088
|
+
key: t.key,
|
|
1089
|
+
cwd: t.cwd || '',
|
|
1090
|
+
name: t.name || '',
|
|
1091
|
+
summary: t.summary || '',
|
|
1092
|
+
waitingUntil: t.waitingUntil,
|
|
1093
|
+
remainSec: sec,
|
|
1094
|
+
remainMs: Math.max(0, st.deadline - now),
|
|
1095
|
+
label: `${sec}s`,
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
} else if (st && st.phase === 'waiting') {
|
|
1099
|
+
const outcome = consumedCount(t) > st.consumed0 ? 'delivered' : 'timeout';
|
|
1100
|
+
state.set(t.key, { phase: 'ended' });
|
|
1101
|
+
yield { type: 'resolved', key: t.key, cwd: t.cwd || '', name: t.name || '', outcome };
|
|
1102
|
+
} else if (!st) {
|
|
1103
|
+
state.set(t.key, { phase: 'idle' });
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// 兜底:waiting 标志还在但截止已过(wait 进程异常退出),也派生一次 timeout
|
|
1107
|
+
if (t.waiting && (!Number.isFinite(deadline) || deadline <= now)) {
|
|
1108
|
+
const cur = state.get(t.key);
|
|
1109
|
+
if (!cur || cur.phase === 'waiting') {
|
|
1110
|
+
state.set(t.key, { phase: 'ended' });
|
|
1111
|
+
yield {
|
|
1112
|
+
type: 'resolved',
|
|
1113
|
+
key: t.key,
|
|
1114
|
+
cwd: t.cwd || '',
|
|
1115
|
+
name: t.name || '',
|
|
1116
|
+
outcome: 'timeout',
|
|
1117
|
+
stale: true,
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
first = false;
|
|
1124
|
+
await sleep(pollMs);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// HTTP 传输:SSE。注册到 sseConns 让 panelPresence 计入(wait 据此判活标签)。
|
|
1129
|
+
// 客户端断开即中止生成器。SSE 心跳由 addEventStreamConn 统一管。
|
|
1130
|
+
export async function streamEventsSSE(req, res, { cwd, key } = {}) {
|
|
1131
|
+
res.writeHead(200, {
|
|
1132
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
1133
|
+
'cache-control': 'no-store',
|
|
1134
|
+
connection: 'keep-alive',
|
|
1135
|
+
});
|
|
1136
|
+
res.write(': connected\n\n');
|
|
1137
|
+
// addEventStreamConn 内部已把 conn 加入 sseConns(panelPresence 据此统计活标签),
|
|
1138
|
+
// 我们不再直接引用——心跳 / 失败剔除 / 断开清理都由它托管。
|
|
1139
|
+
addEventStreamConn(req, res);
|
|
1140
|
+
const ac = new AbortController();
|
|
1141
|
+
req.on('close', () => ac.abort());
|
|
1142
|
+
try {
|
|
1143
|
+
for await (const ev of watchEvents({ cwd, key, signal: ac.signal })) {
|
|
1144
|
+
res.write(`event: ${ev.type}\ndata: ${JSON.stringify(ev)}\n\n`);
|
|
1145
|
+
}
|
|
1146
|
+
} catch {
|
|
1147
|
+
/* 客户端断开 / 瞬时读盘错误,静默结束 */
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
@@ -54,6 +54,11 @@ export default function SessionTaskView() {
|
|
|
54
54
|
const [suffixEditing, setSuffixEditing] = useState(false);
|
|
55
55
|
const [suffixDraft, setSuffixDraft] = useState('');
|
|
56
56
|
const [suffixSaving, setSuffixSaving] = useState(false);
|
|
57
|
+
// 探活状态:'connecting' | 'ok' | 'reconnecting'(数据轮询 + 后续 SSE 接入)
|
|
58
|
+
const [liveStatus, setLiveStatus] = useState('connecting');
|
|
59
|
+
// 「只读弹窗」:点击现状/上一轮总结/任务文本时显示聚焦阅读的模态框
|
|
60
|
+
// modal = { title, body } | null
|
|
61
|
+
const [modal, setModal] = useState(null);
|
|
57
62
|
const [editingId, setEditingId] = useState(null);
|
|
58
63
|
const [editText, setEditText] = useState('');
|
|
59
64
|
const [editSaving, setEditSaving] = useState(false);
|
|
@@ -69,11 +74,14 @@ export default function SessionTaskView() {
|
|
|
69
74
|
|
|
70
75
|
const load = useCallback(async () => {
|
|
71
76
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
const data = await api('/api/localtasks');
|
|
78
|
+
setOverview(data);
|
|
79
|
+
setLiveStatus('ok');
|
|
80
|
+
} catch {
|
|
81
|
+
// 轮询失败 → 进入「重连中」态;指示灯变红,避免每 1.5s 弹错误 toast
|
|
82
|
+
setLiveStatus('reconnecting');
|
|
75
83
|
}
|
|
76
|
-
}, [
|
|
84
|
+
}, []);
|
|
77
85
|
|
|
78
86
|
useEffect(() => {
|
|
79
87
|
load();
|
|
@@ -85,6 +93,16 @@ export default function SessionTaskView() {
|
|
|
85
93
|
return () => clearInterval(t);
|
|
86
94
|
}, [load]);
|
|
87
95
|
|
|
96
|
+
// 只读弹窗:按 Esc 关闭
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!modal) return;
|
|
99
|
+
const onKey = (e) => {
|
|
100
|
+
if (e.key === 'Escape') setModal(null);
|
|
101
|
+
};
|
|
102
|
+
window.addEventListener('keydown', onKey);
|
|
103
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
104
|
+
}, [modal]);
|
|
105
|
+
|
|
88
106
|
const topics = overview ? overview.topics : [];
|
|
89
107
|
|
|
90
108
|
// 同步页面上的超时配置(来源:后端 settings)
|
|
@@ -495,7 +513,35 @@ export default function SessionTaskView() {
|
|
|
495
513
|
style={{ margin: 0 }}
|
|
496
514
|
/>
|
|
497
515
|
<h3>主题总览(按工作目录)</h3>
|
|
498
|
-
<span
|
|
516
|
+
<span
|
|
517
|
+
className="muted"
|
|
518
|
+
style={{
|
|
519
|
+
fontSize: 11,
|
|
520
|
+
display: 'inline-flex',
|
|
521
|
+
alignItems: 'center',
|
|
522
|
+
gap: 4,
|
|
523
|
+
color: liveStatus === 'ok' ? '#2a9d5c' : liveStatus === 'reconnecting' ? '#c0392b' : '#888',
|
|
524
|
+
}}
|
|
525
|
+
title={
|
|
526
|
+
liveStatus === 'ok'
|
|
527
|
+
? '实时连接正常(数据轮询可达后端)'
|
|
528
|
+
: liveStatus === 'reconnecting'
|
|
529
|
+
? '连接断开,正在重连…'
|
|
530
|
+
: '正在连接…'
|
|
531
|
+
}
|
|
532
|
+
>
|
|
533
|
+
<span
|
|
534
|
+
style={{
|
|
535
|
+
display: 'inline-block',
|
|
536
|
+
width: 8,
|
|
537
|
+
height: 8,
|
|
538
|
+
borderRadius: '50%',
|
|
539
|
+
background: liveStatus === 'ok' ? '#2a9d5c' : liveStatus === 'reconnecting' ? '#c0392b' : '#aaa',
|
|
540
|
+
}}
|
|
541
|
+
/>
|
|
542
|
+
{liveStatus === 'ok' ? '实时连接正常' : liveStatus === 'reconnecting' ? '连接断开,正在重连…' : '正在连接…'}
|
|
543
|
+
</span>
|
|
544
|
+
<span className="muted">{overview ? ` · ${topics.length} 个 · 等待中 ${overview.waiting}` : ''}</span>
|
|
499
545
|
<span style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
|
|
500
546
|
<button
|
|
501
547
|
className="btn small ghost"
|
|
@@ -557,7 +603,12 @@ export default function SessionTaskView() {
|
|
|
557
603
|
</div>
|
|
558
604
|
</div>
|
|
559
605
|
{t.summary && (
|
|
560
|
-
<div
|
|
606
|
+
<div
|
|
607
|
+
className="muted"
|
|
608
|
+
style={{ fontSize: 11, whiteSpace: 'pre-wrap', marginBottom: 6, cursor: 'zoom-in' }}
|
|
609
|
+
title="点击查看完整现状"
|
|
610
|
+
onClick={(e) => { e.stopPropagation(); setModal({ title: `主题「${t.name}」现状`, body: t.summary }); }}
|
|
611
|
+
>
|
|
561
612
|
现状:{t.summary}
|
|
562
613
|
</div>
|
|
563
614
|
)}
|
|
@@ -712,12 +763,22 @@ export default function SessionTaskView() {
|
|
|
712
763
|
</>
|
|
713
764
|
) : (
|
|
714
765
|
<>
|
|
715
|
-
|
|
766
|
+
<span
|
|
767
|
+
style={{ cursor: t.text.length > 50 ? 'zoom-in' : 'default' }}
|
|
768
|
+
onClick={(e) => {
|
|
769
|
+
e.stopPropagation();
|
|
770
|
+
if (t.text.length > 50) setModal({ title: `任务 #${t.id} 全文`, body: t.text });
|
|
771
|
+
}}
|
|
772
|
+
title={t.text.length > 50 ? '点击查看完整任务文本' : undefined}
|
|
773
|
+
>
|
|
774
|
+
{t.text}
|
|
775
|
+
</span>
|
|
716
776
|
{t.roundSummary && (
|
|
717
777
|
<div
|
|
718
778
|
className="muted"
|
|
719
|
-
style={{ fontSize: 11, marginTop: 4, paddingTop: 4, borderTop: '1px dashed var(--line,#e6e8eb)', whiteSpace: 'pre-wrap' }}
|
|
720
|
-
title="
|
|
779
|
+
style={{ fontSize: 11, marginTop: 4, paddingTop: 4, borderTop: '1px dashed var(--line,#e6e8eb)', whiteSpace: 'pre-wrap', cursor: 'zoom-in' }}
|
|
780
|
+
title="点击查看完整上一轮总结"
|
|
781
|
+
onClick={(e) => { e.stopPropagation(); setModal({ title: '上一轮完成总结', body: t.roundSummary }); }}
|
|
721
782
|
>
|
|
722
783
|
上一轮完成:{t.roundSummary}
|
|
723
784
|
</div>
|
|
@@ -777,6 +838,38 @@ export default function SessionTaskView() {
|
|
|
777
838
|
)}
|
|
778
839
|
|
|
779
840
|
<CliHints module="localtask" />
|
|
841
|
+
|
|
842
|
+
{modal && (
|
|
843
|
+
<div
|
|
844
|
+
onClick={() => setModal(null)}
|
|
845
|
+
style={{
|
|
846
|
+
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)',
|
|
847
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
|
|
848
|
+
}}
|
|
849
|
+
>
|
|
850
|
+
<div
|
|
851
|
+
onClick={(e) => e.stopPropagation()}
|
|
852
|
+
style={{
|
|
853
|
+
background: '#fff', borderRadius: 8, padding: 20, maxWidth: 'min(720px, 90vw)',
|
|
854
|
+
maxHeight: '80vh', overflow: 'auto', boxShadow: '0 12px 40px rgba(0,0,0,0.25)',
|
|
855
|
+
}}
|
|
856
|
+
>
|
|
857
|
+
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 12 }}>
|
|
858
|
+
<strong style={{ fontSize: 14 }}>{modal.title}</strong>
|
|
859
|
+
<button
|
|
860
|
+
className="btn small ghost"
|
|
861
|
+
onClick={() => setModal(null)}
|
|
862
|
+
style={{ marginLeft: 'auto' }}
|
|
863
|
+
>
|
|
864
|
+
关闭
|
|
865
|
+
</button>
|
|
866
|
+
</div>
|
|
867
|
+
<div style={{ fontSize: 13, lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
|
868
|
+
{modal.body}
|
|
869
|
+
</div>
|
|
870
|
+
</div>
|
|
871
|
+
</div>
|
|
872
|
+
)}
|
|
780
873
|
</>
|
|
781
874
|
);
|
|
782
875
|
}
|
package/src/runtime/api.js
CHANGED
|
@@ -92,6 +92,11 @@ export async function handleApi(req, res, url) {
|
|
|
92
92
|
} else {
|
|
93
93
|
Object.assign(raw, await readBody(req).catch(() => ({})));
|
|
94
94
|
}
|
|
95
|
+
// stream 模式:把 req/res/url 透传给 run,让 action 自己接管响应(SSE 等)
|
|
96
|
+
if (action.stream) {
|
|
97
|
+
await action.run({ ...applySpec(action, raw), __raw: { req, res, url } }, { transport: 'http' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
95
100
|
const data = await action.run(applySpec(action, raw), { transport: 'http' });
|
|
96
101
|
sendJson(res, 200, { ok: true, data });
|
|
97
102
|
} catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e,s as t,t as n}from"./index-
|
|
1
|
+
import{o as e,s as t,t as n}from"./index-2OK9SG2y.js";var r=t();function i({module:t}){let{boot:i}=e(),a=(i?.commands||[]).filter(e=>e.module===t);return a.length?(0,r.jsxs)(`div`,{className:`cli-hint`,children:[(0,r.jsx)(`span`,{className:`cli-hint-label`,children:`CLI 等价(同构命令,加 --json 得机器可读输出):`}),a.map(e=>(0,r.jsx)(n,{className:`cli-cmd`,text:e.command,title:`点击复制:${e.usage}`,children:e.command},e.id))]}):null}export{i as t};
|