@yss123/cage-admin 0.1.0 → 0.3.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.
Files changed (53) hide show
  1. package/README.md +62 -3
  2. package/dist/bootstrap.d.ts +27 -1
  3. package/dist/bootstrap.js +52 -16
  4. package/dist/bootstrap.js.map +1 -1
  5. package/dist/catalog.d.ts +48 -0
  6. package/dist/catalog.js +96 -0
  7. package/dist/catalog.js.map +1 -0
  8. package/dist/diagnostics.d.ts +14 -0
  9. package/dist/diagnostics.js +163 -0
  10. package/dist/diagnostics.js.map +1 -0
  11. package/dist/domain/constraints.d.ts +23 -0
  12. package/dist/domain/constraints.js +27 -0
  13. package/dist/domain/constraints.js.map +1 -1
  14. package/dist/domain/index.d.ts +5 -0
  15. package/dist/domain/index.js +5 -0
  16. package/dist/domain/index.js.map +1 -1
  17. package/dist/domain/org.d.ts +230 -149
  18. package/dist/domain/org.js +98 -4
  19. package/dist/domain/org.js.map +1 -1
  20. package/dist/http.d.ts +13 -1
  21. package/dist/http.js +28 -13
  22. package/dist/http.js.map +1 -1
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.js +6 -0
  25. package/dist/index.js.map +1 -1
  26. package/package.json +9 -5
  27. package/ui/dist/app/shell.js +135 -0
  28. package/ui/dist/app.js +34 -0
  29. package/ui/dist/modules/audit/audit-page.js +107 -0
  30. package/ui/dist/modules/audit/index.js +5 -0
  31. package/ui/dist/modules/auth/index.js +5 -0
  32. package/ui/dist/modules/auth/login-page.js +38 -0
  33. package/ui/dist/modules/models/index.js +5 -0
  34. package/ui/dist/modules/models/models-page.js +60 -0
  35. package/ui/dist/modules/operations/index.js +6 -0
  36. package/ui/dist/modules/operations/operations-page.js +110 -0
  37. package/ui/dist/modules/operations/sync-page.js +60 -0
  38. package/ui/dist/modules/org/index.js +5 -0
  39. package/ui/dist/modules/org/org-page.js +243 -0
  40. package/ui/dist/modules/personalities/index.js +5 -0
  41. package/ui/dist/modules/personalities/personalities-page.js +161 -0
  42. package/ui/dist/modules/roles/index.js +5 -0
  43. package/ui/dist/modules/roles/roles-page.js +348 -0
  44. package/ui/dist/modules/tasks/index.js +5 -0
  45. package/ui/dist/modules/tasks/tasks-page.js +68 -0
  46. package/ui/dist/shared/api.js +166 -0
  47. package/ui/dist/shared/dom.js +139 -0
  48. package/ui/dist/shared/labels.js +84 -0
  49. package/ui/dist/shared/navigation.js +14 -0
  50. package/ui/dist/shared/types.js +1 -0
  51. package/ui/index.html +12 -10
  52. package/ui/style.css +1657 -41
  53. package/ui/app.js +0 -339
@@ -0,0 +1,243 @@
1
+ /**
2
+ * 组织管理页面:按读取能力分别加载部门、用户与角色,再提供受能力控制的组织命令入口。
3
+ * 部门负责人用于审批选人,角色用于授权,两种配置独立维护;页面显示只反映当前取回的分页数据。
4
+ */
5
+ import { api, can, run } from '../../shared/api.js';
6
+ import { badge, field, h, icon, modal, panel, searchInput, table } from '../../shared/dom.js';
7
+ import { navigate } from '../../shared/navigation.js';
8
+ /** 组织编辑继续走生成命令;负责人和角色是两份配置,在界面上明确区分。 */
9
+ /**
10
+ * 并行读取最多一页组织数据,缺读取能力的集合按空列表处理,后端继续约束实际范围。
11
+ * 部门用 ancestors 深度显示层级;用户列表支持本地搜索,写入通过生成的领域路由,成功后刷新管理页面。
12
+ */
13
+ export async function renderOrg(main) {
14
+ const [departments, people, roleResult] = await Promise.all([
15
+ can('Dept.read')
16
+ ? api('GET', '/api/dept?pageSize=200&sort=createdAt')
17
+ : Promise.resolve({ items: [], total: 0 }),
18
+ can('User.read')
19
+ ? api('GET', '/api/user?pageSize=200&status=active,disabled')
20
+ : Promise.resolve({ items: [], total: 0 }),
21
+ can('Role.read')
22
+ ? api('GET', '/api/role?pageSize=200')
23
+ : Promise.resolve({ items: [] }),
24
+ ]);
25
+ const depts = departments.items, users = people.items, roles = roleResult.items;
26
+ const departmentName = new Map(depts.map((dept) => [dept.id, dept.name]));
27
+ const userName = new Map(users.map((user) => [user.id, `${user.name}(${user.username})`]));
28
+ const roleName = new Map(roles.map((role) => [role.id, role.name]));
29
+ const depth = (dept) => Math.max(0, dept.ancestors.split('/').filter(Boolean).length - 1);
30
+ const selectDepartment = (value) => h('select', { ...(value ? { value } : {}) }, depts.map((dept) => h('option', { value: dept.id }, `${' '.repeat(depth(dept))}${dept.name}`)));
31
+ main.append(h('div', { class: 'stats-row' }, stat('部门', departments.total, 'grid'), stat('用户', people.total, 'users'), stat('当前列表已启用', users.filter((user) => user.status === 'active').length, 'check')));
32
+ const deptPanel = panel('部门与负责人', '部门负责人用于审批选人;给用户分配经理角色后,仍需在这里指定负责人。');
33
+ const departmentActions = h('div', { class: 'section-actions' });
34
+ if (can('Dept.create') && depts.length)
35
+ departmentActions.append(h('button', { class: 'small', onclick: createDepartment }, icon('plus'), '新建部门'));
36
+ deptPanel.querySelector('.panel-heading').append(departmentActions);
37
+ deptPanel.append(table([
38
+ [
39
+ '部门名称',
40
+ (dept) => h('div', { class: 'entity-cell', style: `padding-left:${depth(dept) * 20}px` }, h('span', { class: 'entity-icon' }, icon('grid')), h('strong', {}, dept.name)),
41
+ ],
42
+ ['部门编码', (dept) => h('code', { class: 'muted' }, dept.code || '—')],
43
+ [
44
+ '部门负责人',
45
+ (dept) => {
46
+ if (!can('Dept.update') || !can('User.read'))
47
+ return userName.get(dept.leaderId ?? '') || '未设置';
48
+ const select = h('select', {
49
+ value: dept.leaderId ?? '',
50
+ 'aria-label': `${dept.name}负责人`,
51
+ onchange: async () => {
52
+ select.disabled = true;
53
+ const updated = await run(() => api('POST', `/api/dept/${dept.id}/update`, { leaderId: select.value || null }), '部门负责人已保存');
54
+ if (updated)
55
+ dept.leaderId = updated.leaderId;
56
+ else
57
+ select.value = dept.leaderId ?? '';
58
+ select.disabled = false;
59
+ },
60
+ }, h('option', { value: '' }, '请选择负责人'), users
61
+ .filter((user) => user.status === 'active')
62
+ .map((user) => h('option', { value: user.id }, userName.get(user.id))));
63
+ return select;
64
+ },
65
+ ],
66
+ [
67
+ '状态',
68
+ (dept) => badge(dept.status === 'active' ? '已启用' : '已停用', dept.status === 'active' ? 'green' : 'neutral'),
69
+ ],
70
+ ], depts));
71
+ if (can('Dept.read'))
72
+ main.append(deptPanel);
73
+ if (can('User.read')) {
74
+ const out = h('div');
75
+ let query = '';
76
+ const toolbar = h('div', { class: 'table-toolbar' }, searchInput('搜索姓名、账号或部门', (value) => {
77
+ query = value.toLowerCase();
78
+ renderUsers();
79
+ }));
80
+ if (can('User.create'))
81
+ toolbar.append(h('button', { class: 'primary', disabled: !depts.length, onclick: createUser }, icon('plus'), '新建用户'));
82
+ const userPanel = panel('用户与角色', '通过角色分配操作权限;账号停用后将无法继续访问系统。', toolbar, out);
83
+ main.append(userPanel);
84
+ /**
85
+ * 按当前本地搜索条件重画已加载用户,并根据命令能力展示角色、密码、启停操作。
86
+ * 过滤范围限于当前返回的数据,不执行服务器全量搜索。
87
+ */
88
+ function renderUsers() {
89
+ const visible = users.filter((user) => `${user.name} ${user.username} ${departmentName.get(user.deptId) ?? ''}`
90
+ .toLowerCase()
91
+ .includes(query));
92
+ out.replaceChildren(table([
93
+ [
94
+ '用户',
95
+ (user) => h('div', { class: 'person-cell' }, h('span', { class: 'person-avatar' }, user.name.slice(0, 1)), h('div', {}, h('strong', {}, user.name), h('small', {}, user.username))),
96
+ ],
97
+ ['所属部门', (user) => departmentName.get(user.deptId) || '未获取部门名称'],
98
+ [
99
+ '拥有角色',
100
+ (user) => h('div', { class: 'tag-list' }, user.roleIds.length
101
+ ? user.roleIds.map((id) => badge(roleName.get(id) || '未获取角色名称'))
102
+ : h('span', { class: 'muted' }, '尚未分配'), can('User.assignRoles') && can('Role.read') && user.status === 'active'
103
+ ? h('button', { class: 'text-button', onclick: () => assignRoles(user) }, '配置')
104
+ : null),
105
+ ],
106
+ [
107
+ '状态',
108
+ (user) => badge(user.status === 'active' ? '已启用' : '已停用', user.status === 'active' ? 'green' : 'neutral'),
109
+ ],
110
+ [
111
+ '账号操作',
112
+ (user) => h('div', { class: 'row-actions' }, can('User.setPassword') && user.status === 'active'
113
+ ? h('button', { class: 'text-button', onclick: () => resetPassword(user) }, '重置密码')
114
+ : null, user.status === 'active' && can('User.disable')
115
+ ? h('button', {
116
+ class: 'text-button danger',
117
+ onclick: async (event) => {
118
+ if (await run(() => api('POST', `/api/user/${user.id}/disable`), '账号已停用', event.currentTarget))
119
+ navigate('org');
120
+ },
121
+ }, '停用')
122
+ : null, user.status === 'disabled' && can('User.enable')
123
+ ? h('button', {
124
+ class: 'text-button',
125
+ onclick: async (event) => {
126
+ if (await run(() => api('POST', `/api/user/${user.id}/enable`), '账号已启用', event.currentTarget))
127
+ navigate('org');
128
+ },
129
+ }, '启用')
130
+ : null),
131
+ ],
132
+ ], visible));
133
+ }
134
+ renderUsers();
135
+ }
136
+ if (people.total > users.length || departments.total > depts.length)
137
+ main.append(h('p', { class: 'muted' }, '当前仅展示前 200 条可见组织记录;超出部分请通过业务组织接口查询。'));
138
+ /**
139
+ * 打开新建部门表单,通过 Dept.create 传入父部门与资料;成功关闭弹窗并重载组织页。
140
+ */
141
+ function createDepartment() {
142
+ const name = h('input', { required: true, maxlength: 100, placeholder: '例如:采购部' });
143
+ const parent = selectDepartment();
144
+ const save = h('button', { type: 'submit', class: 'primary' }, '创建部门');
145
+ const form = h('form', {
146
+ class: 'stack-form',
147
+ onsubmit: async (event) => {
148
+ event.preventDefault();
149
+ if (await run(() => api('POST', '/api/dept/create', { name: name.value, parentId: parent.value }), '部门已创建', save)) {
150
+ dialog.close();
151
+ navigate('org');
152
+ }
153
+ },
154
+ }, field('部门名称', name), field('上级部门', parent), h('div', { class: 'form-actions' }, save));
155
+ const dialog = modal('新建部门', form);
156
+ name.focus();
157
+ }
158
+ /**
159
+ * 收集用户名、姓名及部门并调用用户创建命令;成功后刷新组织页,密码和角色需通过独立操作配置。
160
+ * 页面成功动作基于实际请求结果,不通过直接写表配置账号。
161
+ */
162
+ function createUser() {
163
+ const username = h('input', { required: true, placeholder: '用于登录的账号', autocomplete: 'off' });
164
+ const name = h('input', { required: true, placeholder: '用户真实姓名' });
165
+ const department = selectDepartment();
166
+ const save = h('button', { type: 'submit', class: 'primary' }, '创建用户');
167
+ const form = h('form', {
168
+ class: 'stack-form',
169
+ onsubmit: async (event) => {
170
+ event.preventDefault();
171
+ if (await run(() => api('POST', '/api/user/create', {
172
+ username: username.value,
173
+ name: name.value,
174
+ deptId: department.value,
175
+ }), '用户已创建,请设置密码并分配角色', save)) {
176
+ dialog.close();
177
+ navigate('org');
178
+ }
179
+ },
180
+ }, field('登录账号', username), field('用户姓名', name), field('所属部门', department), h('p', { class: 'inline-info' }, icon('info'), '新用户默认没有角色。创建后请设置登录密码,并按职责配置角色。'), h('div', { class: 'form-actions' }, save));
181
+ const dialog = modal('新建用户', form);
182
+ username.focus();
183
+ }
184
+ /**
185
+ * 编辑所选用户的完整角色 ID 集合,已有选择作为初值,保存走 User.assignRoles。
186
+ * 界面提示不能代替服务端防提权和角色有效性检查,成功后才关闭并刷新。
187
+ */
188
+ function assignRoles(user) {
189
+ const selected = new Set(user.roleIds);
190
+ const save = h('button', { class: 'primary', type: 'submit' }, '保存角色');
191
+ const form = h('form', {
192
+ class: 'stack-form',
193
+ onsubmit: async (event) => {
194
+ event.preventDefault();
195
+ if (await run(() => api('POST', `/api/user/${user.id}/assign-roles`, { roleIds: [...selected] }), '用户角色已保存', save)) {
196
+ dialog.close();
197
+ navigate('org');
198
+ }
199
+ },
200
+ }, h('p', { class: 'muted' }, `为 ${user.name} 选择角色,多个角色的有效权限会共同生效。`), h('div', { class: 'choice-list' }, roles
201
+ .filter((role) => role.status === 'active')
202
+ .map((role) => h('label', {}, h('input', {
203
+ type: 'checkbox',
204
+ checked: selected.has(role.id),
205
+ onchange: (event) => {
206
+ event.currentTarget.checked
207
+ ? selected.add(role.id)
208
+ : selected.delete(role.id);
209
+ },
210
+ }), h('span', {}, h('strong', {}, role.name), h('small', {}, role.code))))), h('div', { class: 'form-actions' }, save));
211
+ const dialog = modal('配置用户角色', form);
212
+ }
213
+ /**
214
+ * 为指定用户打开密码重置表单,提交到独立 set-password 命令。
215
+ * 密码仅保留在表单和请求中;输入校验/授权由服务端再次执行,不影响角色分配。
216
+ */
217
+ function resetPassword(user) {
218
+ const password = h('input', {
219
+ type: 'password',
220
+ minlength: 8,
221
+ required: true,
222
+ autocomplete: 'new-password',
223
+ placeholder: '至少 8 位',
224
+ });
225
+ const save = h('button', { type: 'submit', class: 'primary' }, '保存新密码');
226
+ const form = h('form', {
227
+ class: 'stack-form',
228
+ onsubmit: async (event) => {
229
+ event.preventDefault();
230
+ if (await run(() => api('POST', `/api/user/${user.id}/set-password`, { password: password.value }), '密码已重置', save))
231
+ dialog.close();
232
+ },
233
+ }, h('p', { class: 'muted' }, `正在为 ${user.name}(${user.username})设置新密码。`), field('新密码', password), h('div', { class: 'form-actions' }, save));
234
+ const dialog = modal('重置登录密码', form);
235
+ password.focus();
236
+ }
237
+ }
238
+ /**
239
+ * 生成组织页的统计展示卡;value 由调用者传入,不能把本页条数误当服务器总数。
240
+ */
241
+ function stat(label, value, glyph) {
242
+ return h('div', { class: 'stat-card' }, h('span', { class: 'stat-icon' }, icon(glyph)), h('div', {}, h('span', {}, label), h('strong', {}, value)));
243
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 助理性格配置与试聊模块的公开入口。
3
+ * 应用壳通过这里组合页面,其他模块无需依赖内部表单实现;导出本身不发请求或执行管理操作。
4
+ */
5
+ export { renderPersonalities } from './personalities-page.js';
@@ -0,0 +1,161 @@
1
+ import { api, postEvents, run } from '../../shared/api.js';
2
+ import { badge, field, h } from '../../shared/dom.js';
3
+ /** 编辑器只提交框架协议;配置与试聊的权限、模型和持久化全部由 AI 运行时处理。 */
4
+ /**
5
+ * 读取性格目录并装配默认选择、卡片、编辑器和试聊区。
6
+ * 保存期间冻结表单,成功才清草稿并替换服务端版本;失败保留输入,finally 恢复控件。
7
+ * 试聊逐段追加文本并在 done 时校准最终回复;断流和取消保留已收到内容并给出明确状态。
8
+ */
9
+ export async function renderPersonalities(main) {
10
+ let catalog = await api('GET', '/api/ai/admin/personalities');
11
+ let selected = catalog.settings.defaultKey, saving = false, activePreview = null;
12
+ const drafts = new Map();
13
+ const grid = h('div', { class: 'persona-grid', 'aria-label': '性格列表' });
14
+ const editor = h('div', { class: 'persona-editor-area' });
15
+ const defaults = h('div', { class: 'persona-default' });
16
+ const current = () => drafts.get(selected) ?? catalog.items.find(p => p.key === selected);
17
+ /**
18
+ * 重建默认性格选择,只列已启用项;提交携带 settings.version,成功后更新目录和默认标记。
19
+ */
20
+ function drawDefault() {
21
+ const select = h('select', { 'aria-label': '默认性格', value: catalog.settings.defaultKey }, catalog.items.filter(p => p.enabled).map(p => h('option', { value: p.key }, `${p.avatar} ${p.label}`)));
22
+ const save = h('button', { onclick: async () => {
23
+ const result = await run(() => api('POST', '/api/ai/admin/assistant-settings', {
24
+ version: catalog.settings.version, defaultKey: select.value,
25
+ }), '默认性格已更新', save);
26
+ if (result) {
27
+ catalog.settings = result;
28
+ drawCards();
29
+ drawDefault();
30
+ }
31
+ } }, '设为默认');
32
+ defaults.replaceChildren(h('div', {}, h('strong', {}, '默认的相处方式'), h('p', {}, '跟随默认的会话,从下一条消息开始使用此性格。')), h('div', { class: 'persona-default-actions' }, select, save));
33
+ }
34
+ /**
35
+ * 合并已保存目录与新建草稿生成卡片;选择只改变本页编辑对象,保存期间禁止切换。
36
+ */
37
+ function drawCards() {
38
+ const all = new Map(catalog.items.map(p => [p.key, p]));
39
+ for (const [key, draft] of drafts)
40
+ if (!all.has(key))
41
+ all.set(key, draft);
42
+ grid.replaceChildren(...[...all.values()].map(profile => h('button', { class: `persona-card${selected === profile.key ? ' selected' : ''}`,
43
+ 'aria-pressed': selected === profile.key, onclick: () => { if (saving)
44
+ return; selected = profile.key; drawCards(); drawEditor(); },
45
+ }, h('span', { class: 'persona-avatar', 'aria-hidden': 'true' }, profile.avatar), h('span', { class: 'persona-card-copy' }, h('strong', {}, profile.label), h('small', {}, profile.description)), h('span', { class: 'persona-card-tags' }, profile.key === catalog.settings.defaultKey ? badge('默认', 'green') : !profile.enabled ? badge('已停用') : badge(profile.builtin ? '预设' : '自定义')))));
46
+ }
47
+ /**
48
+ * 取消上一份预览,再依据当前草稿/目录构造编辑器。
49
+ * 表单 input 保存内存草稿;试聊使用当前未保存字段,保存与设默认是独立请求。
50
+ */
51
+ function drawEditor() {
52
+ activePreview?.abort();
53
+ activePreview = null;
54
+ let profile = current();
55
+ const label = h('input', { value: profile.label, required: true, maxlength: 40 });
56
+ const avatar = h('input', { value: profile.avatar, required: true, maxlength: 16 });
57
+ const description = h('textarea', { rows: 5, required: true, maxlength: 3000, value: profile.description, placeholder: '描述你希望助理怎样表达、解释和与你相处…' });
58
+ const verbosity = h('select', { value: profile.verbosity }, ...[['concise', '简短利落'], ['balanced', '详略适中'], ['detailed', '详细解释']].map(([value, text]) => h('option', { value }, text)));
59
+ const emoji = h('select', { value: profile.emoji }, ...[['none', '不使用'], ['light', '偶尔点缀'], ['expressive', '活泼一些']].map(([value, text]) => h('option', { value }, text)));
60
+ const address = h('input', { value: profile.address, maxlength: 40, placeholder: '留空,自然称呼' });
61
+ const examples = [0, 1, 2].map(i => h('input', { value: profile.examples[i] ?? '', maxlength: 400, 'aria-label': `说话示例 ${i + 1}`, placeholder: `示例 ${i + 1}(可选)` }));
62
+ const enabled = h('input', { type: 'checkbox', checked: profile.enabled });
63
+ const status = h('p', { class: 'persona-save-status', role: 'status' }, drafts.has(profile.key) ? '有未保存的修改' : profile.version ? `已保存 · 版本 ${profile.version}` : '框架预设,可直接调整');
64
+ const values = () => ({ label: label.value, avatar: avatar.value, description: description.value,
65
+ verbosity: verbosity.value, emoji: emoji.value, address: address.value,
66
+ examples: examples.map(e => e.value.trim()).filter(Boolean), enabled: enabled.checked });
67
+ const save = h('button', { class: 'primary', type: 'submit' }, '保存性格');
68
+ const form = h('form', { class: 'persona-form', oninput: () => {
69
+ drafts.set(profile.key, { ...profile, ...values() });
70
+ status.textContent = '有未保存的修改';
71
+ }, onsubmit: async (event) => {
72
+ event.preventDefault();
73
+ if (saving)
74
+ return;
75
+ saving = true;
76
+ // 保存期间冻结这份表单,避免请求结束后把后来输入的草稿误标为已保存。
77
+ const controls = [label, avatar, description, verbosity, emoji, address, ...examples, enabled];
78
+ controls.forEach(control => { control.disabled = true; });
79
+ try {
80
+ const result = await run(() => api('POST', `/api/ai/admin/personalities/${encodeURIComponent(profile.key)}`, {
81
+ ...values(), version: profile.version,
82
+ }).catch(error => { status.textContent = error.message; throw error; }), '性格已保存', save);
83
+ if (result) {
84
+ profile = result;
85
+ drafts.delete(result.key);
86
+ catalog.items = [...catalog.items.filter(p => p.key !== result.key), result];
87
+ status.textContent = `已保存 · 版本 ${result.version} · 后续消息生效`;
88
+ drawCards();
89
+ drawDefault();
90
+ }
91
+ }
92
+ finally {
93
+ saving = false;
94
+ controls.forEach(control => { control.disabled = false; });
95
+ }
96
+ } }, h('div', { class: 'persona-section-heading' }, h('span', { class: 'eyebrow' }, 'CHARACTER'), h('h2', {}, '定义它的表达方式')), h('div', { class: 'persona-fields' }, field('性格名称', label), field('头像符号', avatar, '输入一个表情或短符号'), field('回复长度', verbosity), field('表情使用', emoji)), field('性格描述', description), field('如何称呼用户', address), h('div', { class: 'persona-examples' }, h('strong', {}, '说话示例'), h('small', {}, '两三句自然的话,帮助助理找到语气。'), examples), h('label', { class: 'persona-enabled' }, enabled, '允许用户选择此性格'), h('div', { class: 'persona-save-row' }, status, save));
97
+ const output = h('div', { class: 'persona-preview-answer', role: 'log', 'aria-live': 'polite' }, '先打个招呼,听听它会怎么说。');
98
+ const message = h('textarea', { rows: 2, required: true, maxlength: 2000, 'aria-label': '试聊消息', value: '今天事情有点多,你能怎么帮我?' });
99
+ const previewStatus = h('p', { class: 'persona-preview-status', role: 'status' });
100
+ const tryButton = h('button', { class: 'primary', type: 'submit' }, '试聊一下');
101
+ const stop = h('button', { type: 'button', hidden: true, onclick: () => activePreview?.abort() }, '停止');
102
+ const preview = h('form', { class: 'persona-preview', onsubmit: async (event) => {
103
+ event.preventDefault();
104
+ if (activePreview || !form.reportValidity())
105
+ return;
106
+ const controller = new AbortController();
107
+ activePreview = controller;
108
+ tryButton.disabled = true;
109
+ stop.hidden = false;
110
+ output.textContent = '';
111
+ previewStatus.textContent = '正在准备回复…';
112
+ let done = false;
113
+ const abort = () => controller.abort();
114
+ window.addEventListener('hashchange', abort, { once: true });
115
+ window.addEventListener('cage:session-expired', abort, { once: true });
116
+ try {
117
+ await postEvents('/api/ai/admin/personality-preview', { profile: values(), message: message.value }, value => {
118
+ if (value.type === 'delta') {
119
+ output.append(document.createTextNode(value.text));
120
+ previewStatus.textContent = '正在回复…';
121
+ }
122
+ if (value.type === 'done') {
123
+ output.textContent = value.result.text;
124
+ previewStatus.textContent = `试聊完成 · ${(value.result.latencyMs / 1000).toFixed(1)} 秒`;
125
+ done = true;
126
+ }
127
+ if (value.type === 'error')
128
+ throw new Error(value.code === 'RATE_LIMITED' ? '已有试聊正在进行,请稍后重试' : `试聊未完成(${value.code}),请检查模型配置后重试`);
129
+ }, controller.signal);
130
+ if (!done)
131
+ throw new Error('试聊连接中断,请重试');
132
+ }
133
+ catch (error) {
134
+ previewStatus.textContent = controller.signal.aborted ? '已停止,保留已收到的文字' : error.message;
135
+ }
136
+ finally {
137
+ window.removeEventListener('hashchange', abort);
138
+ window.removeEventListener('cage:session-expired', abort);
139
+ if (activePreview === controller)
140
+ activePreview = null;
141
+ tryButton.disabled = false;
142
+ stop.hidden = true;
143
+ }
144
+ } }, h('span', { class: 'eyebrow' }, 'SAY HELLO'), h('h2', {}, '先聊两句'), h('p', { class: 'persona-preview-intro' }, '直接体验当前草稿的语气,不必先保存。这里仅预览表达风格,不处理实际业务。'), output, previewStatus, field('你想说什么', message), h('div', { class: 'persona-preview-actions' }, stop, tryButton));
145
+ editor.replaceChildren(form, preview);
146
+ }
147
+ const create = h('button', { class: 'primary', onclick: () => {
148
+ if (saving)
149
+ return;
150
+ const key = `custom-${crypto.randomUUID()}`;
151
+ drafts.set(key, { key, label: '我的助理', avatar: '✨', description: '', verbosity: 'balanced', address: '', emoji: 'light', examples: [], enabled: true, version: 0, builtin: false, updatedAt: null });
152
+ selected = key;
153
+ drawCards();
154
+ drawEditor();
155
+ editor.scrollIntoView({ behavior: 'smooth', block: 'start' });
156
+ } }, '+ 自定义性格');
157
+ main.append(h('div', { class: 'persona-overview' }, h('div', {}, h('span', { class: 'eyebrow' }, 'A LITTLE CHARACTER'), h('h2', {}, '同样的可靠,多一点个性。'), h('p', {}, '选一种合拍的语气,或创造属于团队的助理。')), create), defaults, grid, editor);
158
+ drawDefault();
159
+ drawCards();
160
+ drawEditor();
161
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 角色能力和范围配置模块的公开入口。
3
+ * 应用壳通过这里组合页面,其他模块无需依赖内部表单实现;导出本身不发请求或执行管理操作。
4
+ */
5
+ export { renderRoles } from './roles-page.js';