@ohos-cpf/3rdloop 0.0.4 → 0.0.6
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.md +119 -128
- package/lib/cli.js +25 -0
- package/lib/serve.js +280 -0
- package/lib/update.js +46 -6
- package/lib/web-ext.js +454 -0
- package/lib/web.js +664 -0
- package/package.json +2 -1
- package/vendor/Server/CLI/opencode/index.js +2 -2
- package/vendor/Server/DbUse/StorageManager.js +7 -1
- package/vendor/Server/Routes/controllers/FlexRunnerController.js +92 -0
- package/vendor/Server/Routes/routes/flexrunner.js +2 -0
- package/vendor/Server/Skills/arkts-code-use/SKILL.md +270 -0
- package/vendor/Server/Skills/arkts-code-use/assets/TEMPLATES.md +367 -0
- package/vendor/Server/Skills/arkts-code-use/references/API_VERIFICATION.md +144 -0
- package/vendor/Server/Skills/arkts-code-use/references/ARKTS_RULES.md +240 -0
- package/vendor/Server/Skills/arkts-code-use/references/CODE_PATTERNS.md +431 -0
- package/vendor/Server/Skills/arkts-code-use/references/SYNTAX_CHECK_GUIDE.md +164 -0
- package/vendor/Server/Skills/arkts-code-use/scripts/verify-arkts.cjs +428 -0
- package/vendor/Server/Skills/gitcode-repo-fork/SKILL.md +310 -0
- package/vendor/Server/Skills/gitcode-repo-fork/assets/FORK_REPORT_TEMPLATE.md +113 -0
- package/vendor/Server/Skills/gitcode-repo-fork/references/FORK_DECISION_GUIDE.md +124 -0
- package/vendor/Server/Skills/gitcode-repo-fork/references/GITCODE_FORK_API.md +95 -0
- package/vendor/Server/Skills/gitcode-repo-fork/scripts/gitcode-fork.cjs +285 -0
- package/vendor/VERSION +3 -3
- package/web/css/arktslibrarycheck.css +322 -0
- package/web/css/codecheck.css +464 -0
- package/web/css/flutterlibrarycheck.css +322 -0
- package/web/css/knowledge.css +332 -0
- package/web/css/loop.css +578 -0
- package/web/css/md-reader.css +240 -0
- package/web/css/rnlibrarycheck.css +322 -0
- package/web/css/theme.css +702 -0
- package/web/index.html +713 -0
- package/web/js/arktslibrarycheck.js +1495 -0
- package/web/js/codecheck.js +1098 -0
- package/web/js/flutterlibrarycheck.js +1447 -0
- package/web/js/health.js +69 -0
- package/web/js/knowledge.js +358 -0
- package/web/js/loop.js +1102 -0
- package/web/js/md-reader.js +435 -0
- package/web/js/navigation.js +238 -0
- package/web/js/rnlibrarycheck.js +1462 -0
- package/web/js/stats.js +110 -0
- package/web/js/theme.js +46 -0
- package/web/js/utils.js +228 -0
- package/web/knowledge.html +146 -0
- package/web/loop.html +219 -0
package/web/js/stats.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2
|
+
* stats.js — 页面访问计数 & Session 调用计数 & Session 并发占用
|
|
3
|
+
*
|
|
4
|
+
* 页面加载时:
|
|
5
|
+
* - 调用 /api/stats/visit 自增访问计数并显示
|
|
6
|
+
* - 调用 /api/stats/session/count 获取 Session 调用次数并显示
|
|
7
|
+
* - 调用 /api/stats/session/concurrency 获取并发占用并显示(15s 轮询刷新)
|
|
8
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
var Stats = {
|
|
12
|
+
_concurrencyTimer: null,
|
|
13
|
+
_concurrencyIntervalMs: 15000,
|
|
14
|
+
|
|
15
|
+
/** 页面加载时初始化统计 */
|
|
16
|
+
init: async function () {
|
|
17
|
+
await Promise.allSettled([
|
|
18
|
+
this._initVisit(),
|
|
19
|
+
this._initSession(),
|
|
20
|
+
this.refreshConcurrency()
|
|
21
|
+
]);
|
|
22
|
+
this._startConcurrencyPolling();
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
/** 访问计数(自增) */
|
|
26
|
+
_initVisit: async function () {
|
|
27
|
+
var el = $('visit-count');
|
|
28
|
+
if (!el) return;
|
|
29
|
+
try {
|
|
30
|
+
var r = await apiFetch('GET', '/api/stats/visit');
|
|
31
|
+
if (r.status === 200 && r.data && r.data.success && r.data.data) {
|
|
32
|
+
el.textContent = formatStatCount(r.data.data.visitCount);
|
|
33
|
+
} else {
|
|
34
|
+
el.textContent = '--';
|
|
35
|
+
}
|
|
36
|
+
} catch (e) {
|
|
37
|
+
el.textContent = '--';
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/** Session 调用计数(只读) */
|
|
42
|
+
_initSession: async function () {
|
|
43
|
+
var el = $('session-count');
|
|
44
|
+
if (!el) return;
|
|
45
|
+
try {
|
|
46
|
+
var r = await apiFetch('GET', '/api/stats/session/count');
|
|
47
|
+
if (r.status === 200 && r.data && r.data.success && r.data.data) {
|
|
48
|
+
el.textContent = formatStatCount(r.data.data.sessionCount);
|
|
49
|
+
} else {
|
|
50
|
+
el.textContent = '--';
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
el.textContent = '--';
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Session 并发占用快照(active/limit/waiting)。
|
|
59
|
+
* 占用率着色:<80% 默认色,≥80% 橙色,打满(active==limit 且有排队)红色。
|
|
60
|
+
*/
|
|
61
|
+
refreshConcurrency: async function () {
|
|
62
|
+
var el = $('session-concurrency');
|
|
63
|
+
var pill = $('concurrency-pill');
|
|
64
|
+
if (!el) return;
|
|
65
|
+
try {
|
|
66
|
+
var r = await apiFetch('GET', '/api/stats/session/concurrency');
|
|
67
|
+
var d = (r.data && r.data.success && r.data.data) ? r.data.data : null;
|
|
68
|
+
if (!d || typeof d.active !== 'number') { el.textContent = '--'; return; }
|
|
69
|
+
|
|
70
|
+
el.textContent = d.active + '/' + d.limit;
|
|
71
|
+
|
|
72
|
+
// 悬停提示:占用率 + 排队数
|
|
73
|
+
if (pill) {
|
|
74
|
+
var title = 'AI Session 并发占用:' + d.active + ' / ' + d.limit +
|
|
75
|
+
'(占用率 ' + (d.utilization || 0) + '%)';
|
|
76
|
+
if (d.waiting > 0) title += '\n排队等待槽位的步骤:' + d.waiting + ' 个';
|
|
77
|
+
title += '\n上限由服务端 MAX_CONCURRENT_SESSIONS 配置';
|
|
78
|
+
pill.title = title;
|
|
79
|
+
|
|
80
|
+
pill.classList.remove('conc-high', 'conc-full');
|
|
81
|
+
if (d.active >= d.limit && d.waiting > 0) {
|
|
82
|
+
pill.classList.add('conc-full');
|
|
83
|
+
} else if (d.active / d.limit >= 0.8) {
|
|
84
|
+
pill.classList.add('conc-high');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
el.textContent = '--';
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/** 启动并发占用轮询(15s,页面卸载时自动随定时器回收) */
|
|
93
|
+
_startConcurrencyPolling: function () {
|
|
94
|
+
if (this._concurrencyTimer) return;
|
|
95
|
+
var self = this;
|
|
96
|
+
this._concurrencyTimer = setInterval(function () {
|
|
97
|
+
self.refreshConcurrency();
|
|
98
|
+
}, this._concurrencyIntervalMs);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** 格式化计数(大数字显示优化) */
|
|
103
|
+
function formatStatCount(count) {
|
|
104
|
+
if (typeof count !== 'number') return String(count);
|
|
105
|
+
if (count >= 10000) return (count / 10000).toFixed(1) + 'w';
|
|
106
|
+
if (count >= 1000) return (count / 1000).toFixed(1) + 'k';
|
|
107
|
+
return String(count);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
window.Stats = Stats;
|
package/web/js/theme.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2
|
+
* theme.js — 日间 / 夜间模式切换
|
|
3
|
+
*
|
|
4
|
+
* - 首次访问时遵循系统偏好 (prefers-color-scheme)
|
|
5
|
+
* - 用户手动切换后保存到 localStorage,后续以此为准
|
|
6
|
+
* - 通过 <html data-theme="light|dark"> 驱动 CSS 变量
|
|
7
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
var Theme = {
|
|
11
|
+
_current: 'light',
|
|
12
|
+
|
|
13
|
+
/** 获取当前主题 */
|
|
14
|
+
get current() { return this._current; },
|
|
15
|
+
|
|
16
|
+
/** 应用主题到 DOM */
|
|
17
|
+
apply: function (theme) {
|
|
18
|
+
this._current = theme;
|
|
19
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
/** 切换主题 */
|
|
23
|
+
toggle: function () {
|
|
24
|
+
var next = this._current === 'light' ? 'dark' : 'light';
|
|
25
|
+
this.apply(next);
|
|
26
|
+
localStorage.setItem('theme', next);
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 初始化:localStorage > 系统偏好 > 默认 light
|
|
31
|
+
*/
|
|
32
|
+
init: function () {
|
|
33
|
+
var saved = localStorage.getItem('theme');
|
|
34
|
+
if (saved === 'light' || saved === 'dark') {
|
|
35
|
+
this.apply(saved);
|
|
36
|
+
} else {
|
|
37
|
+
var prefersDark = window.matchMedia &&
|
|
38
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
39
|
+
this.apply(prefersDark ? 'dark' : 'light');
|
|
40
|
+
}
|
|
41
|
+
var btn = $('themeToggle');
|
|
42
|
+
if (btn) btn.onclick = function () { Theme.toggle(); };
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
window.Theme = Theme;
|
package/web/js/utils.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2
|
+
* utils.js — 共享工具函数
|
|
3
|
+
*
|
|
4
|
+
* 高内聚低耦合:所有面板共用的工具函数集中于此,
|
|
5
|
+
* 各面板 JS(loop.js / knowledge.js / 未来功能)通过全局引用调用。
|
|
6
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/** getElementById 简写 */
|
|
10
|
+
function $(id) { return document.getElementById(id); }
|
|
11
|
+
|
|
12
|
+
/** HTML 转义 */
|
|
13
|
+
function escapeHtml(t) {
|
|
14
|
+
if (t == null) return '';
|
|
15
|
+
var d = document.createElement('div');
|
|
16
|
+
d.textContent = String(t);
|
|
17
|
+
return d.innerHTML;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 内联事件处理器参数转义(兼容 Windows / Mac 路径)。
|
|
22
|
+
*
|
|
23
|
+
* 将任意字符串安全转义后,可嵌入 onclick="fn('...')" 形式的内联处理器:
|
|
24
|
+
* - 位于双引号 HTML 属性内 → " 与 & 需转义为 HTML 实体;
|
|
25
|
+
* - 位于单引号 JS 字符串字面量内 → \ 与 ' 需 JS 转义。
|
|
26
|
+
*
|
|
27
|
+
* 关键点:Windows 路径以反斜杠 \ 作分隔符(如 D:\dir\file.md),直接嵌入
|
|
28
|
+
* JS 字符串字面量时,\t 会被解析为 Tab、\d/\s 等会吞掉反斜杠,导致路径
|
|
29
|
+
* 损坏(ENOENT: no such file or directory)。本函数通过转义反斜杠修复之;
|
|
30
|
+
* Mac/Linux 路径以 / 作分隔符,不受影响。
|
|
31
|
+
*/
|
|
32
|
+
function escapeAttr(s) {
|
|
33
|
+
if (s == null) return '';
|
|
34
|
+
return String(s)
|
|
35
|
+
.replace(/\\/g, '\\\\') // \ → \\ (修复 Windows 路径反斜杠被吞)
|
|
36
|
+
.replace(/'/g, "\\'") // ' → \' (闭合单引号 JS 字符串)
|
|
37
|
+
.replace(/\r/g, '\\r') // 控制符 → JS 转义
|
|
38
|
+
.replace(/\n/g, '\\n')
|
|
39
|
+
.replace(/&/g, '&') // & 必须先于 " 转义,避免实体被二次转义
|
|
40
|
+
.replace(/"/g, '"') // " → " (闭合双引号 HTML 属性)
|
|
41
|
+
.replace(/</g, '<')
|
|
42
|
+
.replace(/>/g, '>');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 截断字符串 */
|
|
46
|
+
function truncate(s, n) {
|
|
47
|
+
if (!s) return '';
|
|
48
|
+
n = n || 60;
|
|
49
|
+
return s.length > n ? s.substring(0, n) + '...' : s;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 格式化时间戳 */
|
|
53
|
+
function formatTime(ts) {
|
|
54
|
+
if (!ts) return '—';
|
|
55
|
+
try {
|
|
56
|
+
return new Date(ts).toLocaleString('zh-CN', {
|
|
57
|
+
month: '2-digit', day: '2-digit',
|
|
58
|
+
hour: '2-digit', minute: '2-digit'
|
|
59
|
+
});
|
|
60
|
+
} catch (e) { return ts; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 复制文本到剪贴板 */
|
|
64
|
+
function copyToClipboard(text) {
|
|
65
|
+
if (!text) return;
|
|
66
|
+
try { navigator.clipboard.writeText(text); } catch (e) {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Toast 通知 */
|
|
70
|
+
function toast(type, msg) {
|
|
71
|
+
var el = document.createElement('div');
|
|
72
|
+
el.className = 'toast ' + type;
|
|
73
|
+
el.textContent = msg;
|
|
74
|
+
var container = $('toastContainer');
|
|
75
|
+
if (!container) return;
|
|
76
|
+
container.appendChild(el);
|
|
77
|
+
setTimeout(function () {
|
|
78
|
+
el.style.opacity = '0';
|
|
79
|
+
el.style.transition = 'opacity 0.3s';
|
|
80
|
+
setTimeout(function () { el.remove(); }, 300);
|
|
81
|
+
}, 4000);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** API 请求封装 */
|
|
85
|
+
var API = {
|
|
86
|
+
_fetch: function (url, opts) {
|
|
87
|
+
return fetch(url, opts).then(function (r) { return r.json(); });
|
|
88
|
+
},
|
|
89
|
+
get: function (url) { return this._fetch(url); },
|
|
90
|
+
post: function (url, body) {
|
|
91
|
+
return this._fetch(url, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify(body)
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
del: function (url) { return this._fetch(url, { method: 'DELETE' }); }
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 通用 API 请求(带超时保护,适合未来功能面板使用)
|
|
102
|
+
* @param {string} method - HTTP 方法
|
|
103
|
+
* @param {string} path - 请求路径
|
|
104
|
+
* @param {object} [body] - 请求体
|
|
105
|
+
* @param {number} [timeoutMs=30000] - 超时(毫秒)
|
|
106
|
+
*/
|
|
107
|
+
async function apiFetch(method, path, body, timeoutMs) {
|
|
108
|
+
timeoutMs = timeoutMs || 30000;
|
|
109
|
+
var t0 = Date.now();
|
|
110
|
+
var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
|
|
111
|
+
if (body) opts.body = JSON.stringify(body);
|
|
112
|
+
|
|
113
|
+
var controller = new AbortController();
|
|
114
|
+
opts.signal = controller.signal;
|
|
115
|
+
var timer = setTimeout(function () { controller.abort(); }, timeoutMs);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
var res = await fetch(path, opts);
|
|
119
|
+
var text = await res.text();
|
|
120
|
+
var data;
|
|
121
|
+
try { data = JSON.parse(text); } catch (e) { data = text; }
|
|
122
|
+
return { status: res.status, data: data, elapsed: Date.now() - t0 };
|
|
123
|
+
} catch (e) {
|
|
124
|
+
if (e.name === 'AbortError') throw new Error('请求超时(' + (timeoutMs / 1000) + 's)');
|
|
125
|
+
throw e;
|
|
126
|
+
} finally {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 循环任务状态标签映射 */
|
|
132
|
+
function statusLabel(s) {
|
|
133
|
+
var m = {
|
|
134
|
+
idle: '空闲', planning: '拆解中', executing: '执行中',
|
|
135
|
+
checking: '审核中', aborting: '任务中止中', passed: '已通过', failed: '未通过',
|
|
136
|
+
aborted: '已终止', error: '错误', loaded: '已加载',
|
|
137
|
+
running: '运行中', completed: '已完成', pending: '待执行',
|
|
138
|
+
ready: '就绪', skipped: '已跳过', blocked: '已阻塞', deadlocked: '死锁'
|
|
139
|
+
};
|
|
140
|
+
return m[s] || s || '—';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 通用分页管理器(供未来功能面板使用)
|
|
145
|
+
* @param {object} opts - 配置项 { apiPath, listContainerId, emptyId, countId, pagerId, renderPage, pageSize }
|
|
146
|
+
* @returns {object} 分页管理器实例
|
|
147
|
+
*/
|
|
148
|
+
function createPagination(opts) {
|
|
149
|
+
var pg = {
|
|
150
|
+
apiPath: opts.apiPath,
|
|
151
|
+
listContainerId: opts.listContainerId,
|
|
152
|
+
emptyId: opts.emptyId,
|
|
153
|
+
countId: opts.countId,
|
|
154
|
+
pagerId: opts.pagerId,
|
|
155
|
+
renderPage: opts.renderPage,
|
|
156
|
+
pageSize: opts.pageSize || 20,
|
|
157
|
+
currentPage: 1,
|
|
158
|
+
total: 0,
|
|
159
|
+
totalPages: 1,
|
|
160
|
+
loading: false
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
pg.load = async function (page) {
|
|
164
|
+
page = page || 1;
|
|
165
|
+
if (pg.loading) return;
|
|
166
|
+
pg.loading = true;
|
|
167
|
+
try {
|
|
168
|
+
// apiPath 可自带查询参数(如 ?taskIdPrefix=xxx_),此时用 & 拼接分页参数
|
|
169
|
+
var sep = pg.apiPath.indexOf('?') >= 0 ? '&' : '?';
|
|
170
|
+
var r = await apiFetch('GET', pg.apiPath + sep + 'page=' + page + '&pageSize=' + pg.pageSize);
|
|
171
|
+
// 兼容两种响应格式:{ success: true } 或 { status: 'ok' }
|
|
172
|
+
if (!r.data || (!r.data.success && r.data.status !== 'ok')) return;
|
|
173
|
+
pg.currentPage = r.data.page || page;
|
|
174
|
+
pg.total = r.data.total || 0;
|
|
175
|
+
pg.totalPages = r.data.totalPages || 1;
|
|
176
|
+
var tasks = r.data.tasks || [];
|
|
177
|
+
var listEl = $(pg.listContainerId);
|
|
178
|
+
if (listEl) listEl.innerHTML = '';
|
|
179
|
+
var emptyEl = $(pg.emptyId);
|
|
180
|
+
if (emptyEl) emptyEl.style.display = tasks.length > 0 ? 'none' : '';
|
|
181
|
+
var countEl = $(pg.countId);
|
|
182
|
+
if (countEl) countEl.textContent = pg.total + ' 个任务';
|
|
183
|
+
pg.renderPage(tasks, page === 1);
|
|
184
|
+
pg._renderPager();
|
|
185
|
+
} catch (e) {
|
|
186
|
+
console.error('分页加载失败 [' + pg.apiPath + ']:', e.message);
|
|
187
|
+
} finally {
|
|
188
|
+
pg.loading = false;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
pg.go = function (page) {
|
|
193
|
+
if (page >= 1 && page <= pg.totalPages) pg.load(page);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
pg._renderPager = function () {
|
|
197
|
+
var container = $(pg.pagerId);
|
|
198
|
+
if (!container) return;
|
|
199
|
+
if (pg.totalPages <= 1) { container.innerHTML = ''; return; }
|
|
200
|
+
var cur = pg.currentPage, total = pg.totalPages;
|
|
201
|
+
var pages = [];
|
|
202
|
+
if (total <= 7) { for (var i = 1; i <= total; i++) pages.push(i); }
|
|
203
|
+
else {
|
|
204
|
+
pages.push(1);
|
|
205
|
+
if (cur > 4) pages.push('...');
|
|
206
|
+
var start = Math.max(2, cur - 1), end = Math.min(total - 1, cur + 1);
|
|
207
|
+
for (var j = start; j <= end; j++) pages.push(j);
|
|
208
|
+
if (cur < total - 3) pages.push('...');
|
|
209
|
+
pages.push(total);
|
|
210
|
+
}
|
|
211
|
+
var fnName = pg.pagerId.replace(/-/g, '_') + '_go';
|
|
212
|
+
var html = '<div class="pagination">';
|
|
213
|
+
html += '<button class="page-btn ' + (cur === 1 ? 'disabled' : '') + '" ' +
|
|
214
|
+
(cur === 1 ? 'disabled' : 'onclick="' + fnName + '(' + (cur - 1) + ')"') + '>‹</button>';
|
|
215
|
+
pages.forEach(function (p) {
|
|
216
|
+
if (p === '...') html += '<span class="page-ellipsis">…</span>';
|
|
217
|
+
else html += '<button class="page-btn ' + (p === cur ? 'active' : '') + '" onclick="' + fnName + '(' + p + ')">' + p + '</button>';
|
|
218
|
+
});
|
|
219
|
+
html += '<button class="page-btn ' + (cur === total ? 'disabled' : '') + '" ' +
|
|
220
|
+
(cur === total ? 'disabled' : 'onclick="' + fnName + '(' + (cur + 1) + ')"') + '>›</button>';
|
|
221
|
+
html += '<span class="page-info">' + cur + '/' + total + ' 页,共 ' + pg.total + ' 条</span>';
|
|
222
|
+
html += '</div>';
|
|
223
|
+
container.innerHTML = html;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
window[pg.pagerId.replace(/-/g, '_') + '_go'] = function (page) { pg.go(page); };
|
|
227
|
+
return pg;
|
|
228
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>📚 知识导入 - AI Loop</title>
|
|
7
|
+
<link rel="stylesheet" href="loop.css">
|
|
8
|
+
<link rel="stylesheet" href="knowledge.css">
|
|
9
|
+
<link rel="stylesheet" href="md-reader.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
|
|
13
|
+
<div class="app-header">
|
|
14
|
+
<h1>📚 知识导入</h1>
|
|
15
|
+
<nav class="nav-links">
|
|
16
|
+
<a href="/" class="nav-link">🔁 循环控制台</a>
|
|
17
|
+
<a href="/knowledge" class="nav-link active">📚 知识导入</a>
|
|
18
|
+
</nav>
|
|
19
|
+
<button class="health-btn" id="healthBtn">
|
|
20
|
+
<span class="health-dot unknown" id="healthDot"></span>
|
|
21
|
+
<span id="healthText">检测中</span>
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="app-body">
|
|
26
|
+
<!-- ─── 左侧:知识列表 ─────────────────────────────────────────── -->
|
|
27
|
+
<div class="kn-sidebar">
|
|
28
|
+
<div class="kn-sidebar-header">
|
|
29
|
+
<h2>📋 知识库 <span class="count-badge" id="knCount">0</span></h2>
|
|
30
|
+
<input class="search-box" id="knSearch" placeholder="搜索任务描述或 ID..." />
|
|
31
|
+
<button class="secondary sm kn-refresh-btn" id="btnKnRefresh">🔄 刷新列表</button>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="kn-list" id="knList">
|
|
34
|
+
<div class="detail-empty">加载中...</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- ─── 右侧:知识详情 + 导入操作 ──────────────────────────────── -->
|
|
39
|
+
<div class="kn-main">
|
|
40
|
+
<!-- 知识详情卡 -->
|
|
41
|
+
<div class="kn-detail-card" id="knDetailCard" style="display:none;">
|
|
42
|
+
<div class="kn-detail-header">
|
|
43
|
+
<div class="kn-detail-title">
|
|
44
|
+
<h2 id="knTitle">—</h2>
|
|
45
|
+
<span class="kn-id" id="knDetailId">—</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="kn-detail-actions">
|
|
48
|
+
<button class="primary sm" id="btnStartImport">📥 开始导入</button>
|
|
49
|
+
<button class="secondary sm" id="btnViewDoc">📄 查看原文</button>
|
|
50
|
+
<button class="danger sm" id="btnDeleteKn">🗑️ 删除知识</button>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="kn-detail-meta" id="knDetailMeta"></div>
|
|
54
|
+
<div class="kn-detail-desc" id="knDetailDesc">—</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- 导入面板 -->
|
|
58
|
+
<div class="kn-import-panel" id="knImportPanel" style="display:none;">
|
|
59
|
+
<div class="kn-import-header">
|
|
60
|
+
<h3>📥 知识导入</h3>
|
|
61
|
+
<span class="muted" id="knImportTarget"></span>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="kn-import-body">
|
|
65
|
+
<div class="form-group">
|
|
66
|
+
<label>导入模式</label>
|
|
67
|
+
<div class="hint" style="margin-top:4px;">🔄 异步导入 — 立即返回 importId,通过轮询查询导入进度(适合长任务)</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="form-group">
|
|
71
|
+
<label>自定义提示词(可选)</label>
|
|
72
|
+
<textarea class="large" id="knUserPrompt" placeholder="输入额外的提示文字,将追加到导入执行 prompt 中... 例如:请重点关注鸿蒙端 API 适配部分,生成可复用的 SKILL 文档"></textarea>
|
|
73
|
+
<div class="hint">提示词会追加到知识导入的执行 prompt 末尾,用于引导 AI 更精准地处理知识</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="advanced-toggle" id="knAdvancedToggle">高级选项</div>
|
|
77
|
+
<div class="advanced-section" id="knAdvancedSection">
|
|
78
|
+
<div class="form-group">
|
|
79
|
+
<label>工作目录(可选)</label>
|
|
80
|
+
<input id="knWorkDir" placeholder="留空则自动推导(基于来源任务 ID)">
|
|
81
|
+
</div>
|
|
82
|
+
<div class="form-group">
|
|
83
|
+
<label>SKILL 目录(可选)</label>
|
|
84
|
+
<input id="knSkillDir" placeholder="留空则使用默认 SKILL 目录">
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div class="kn-import-actions">
|
|
89
|
+
<button class="secondary" id="btnImportCancel">取消</button>
|
|
90
|
+
<button class="primary" id="btnImportSubmit" disabled>📥 执行导入</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- 异步导入状态轮询 -->
|
|
96
|
+
<div class="kn-import-status" id="knImportStatus" style="display:none;">
|
|
97
|
+
<div class="kn-status-header">
|
|
98
|
+
<h3>📊 导入进度</h3>
|
|
99
|
+
<button class="secondary sm" id="btnStopPoll">停止轮询</button>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="kn-status-body" id="knStatusBody"></div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- 空状态 -->
|
|
105
|
+
<div class="kn-empty" id="knEmpty">
|
|
106
|
+
<div class="kn-empty-icon">📚</div>
|
|
107
|
+
<div class="kn-empty-text">请从左侧选择一条知识,或点击「刷新列表」加载知识库</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- ─── 查看原文弹窗 ──────────────────────────────────────────────── -->
|
|
113
|
+
<div class="modal-overlay" id="docOverlay">
|
|
114
|
+
<div class="modal" style="width:820px;">
|
|
115
|
+
<div class="modal-header">
|
|
116
|
+
<h3 id="docTitle">经验文档</h3>
|
|
117
|
+
<button class="modal-close" id="docClose">✕</button>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="modal-body"><pre id="docContent">加载中...</pre></div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- ─── 知识删除确认弹窗 ──────────────────────────────────────────── -->
|
|
124
|
+
<div class="modal-overlay kn-delete-overlay" id="knDeleteOverlay">
|
|
125
|
+
<div class="modal kn-delete-modal">
|
|
126
|
+
<div class="kn-delete-icon">🗑️</div>
|
|
127
|
+
<h3 class="kn-delete-title">删除知识</h3>
|
|
128
|
+
<div class="kn-delete-desc" id="knDelTitle">—</div>
|
|
129
|
+
<div class="kn-delete-id">ID: <span id="knDelId">—</span></div>
|
|
130
|
+
<div class="kn-delete-warning">
|
|
131
|
+
⚠️ 删除后将<strong>永久移除</strong>该知识条目(含索引记录),不可恢复。
|
|
132
|
+
原始经验文档(.md)文件不会被删除,仅从知识库中取消引用。
|
|
133
|
+
</div>
|
|
134
|
+
<div class="kn-delete-actions">
|
|
135
|
+
<button class="secondary" id="btnDeleteCancel">取消</button>
|
|
136
|
+
<button class="danger" id="btnDeleteConfirm">🗑️ 确认删除</button>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div class="toast-container" id="toastContainer"></div>
|
|
142
|
+
|
|
143
|
+
<script src="md-reader.js"></script>
|
|
144
|
+
<script src="knowledge.js"></script>
|
|
145
|
+
</body>
|
|
146
|
+
</html>
|