@deepwhale-cn/dsh-compliance-doc 0.2.1 → 0.2.3

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/lib/api.js CHANGED
@@ -1,7 +1,8 @@
1
1
  /** dsh-compliance-doc · 宿主出口(面板后端) */
2
2
  import { dirname, join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { readFileSync } from 'node:fs';
4
+ import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
5
6
  import { runPanel } from './run.mjs';
6
7
 
7
8
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
@@ -29,6 +30,21 @@ export function registerApi(ctx) {
29
30
  try {
30
31
  const u = new URL(req.url || '/', 'http://127.0.0.1');
31
32
  if (u.pathname === `${ROUTE}/api/info`) return 回(res, 200, { ok: true, version: VERSION });
33
+ if (u.pathname === `${ROUTE}/api/upload` && req.method === 'POST') {
34
+ const b = await 读体(req);
35
+ const files = Array.isArray(b.files) ? b.files : [];
36
+ if (!files.length) return 回(res, 400, { ok: false, error: '没有收到文件' });
37
+ const dir = mkdtempSync(join(tmpdir(), 'dwh-legal-'));
38
+ let total = 0; const paths = [];
39
+ for (const f of files) {
40
+ const name = String(f.name || 'file').replace(/[\\/:*?"<>|]/g, '_');
41
+ const buf = Buffer.from(String(f.data || ''), 'base64');
42
+ total += buf.length;
43
+ if (total > 60 * 1024 * 1024) return 回(res, 413, { ok: false, error: '文件累计超过 60MB' });
44
+ const fp = join(dir, name); writeFileSync(fp, buf); paths.push(fp);
45
+ }
46
+ return 回(res, 200, { ok: true, dir, path: paths.length === 1 ? paths[0] : dir, count: paths.length });
47
+ }
32
48
  if (u.pathname === `${ROUTE}/api/run` && req.method === 'POST') {
33
49
  const body = await 读体(req);
34
50
  return 回(res, 200, runPanel(body));
package/lib/client.js CHANGED
@@ -11,30 +11,36 @@ window.__ModuleLoader__.load({
11
11
  const PROFILES = ['通用', '等保三级'];
12
12
  const FIELDS = [
13
13
  { k: 'input', label: '法律·合规文书', ph: '例如 /Users/mac/scan/report.json' },
14
- { k: 'out', label: '输出目录', ph: '例如 /Users/mac/交付文档' },
14
+ { k: 'out', label: '输出目录', ph: '可留空,默认 报告旁的「合规文书」' },
15
15
  { k: 'org', label: '单位名称', ph: '可留空;对外提交建议填' },
16
16
  { k: 'date', label: '检查日期', ph: 'YYYY-MM-DD,留空取今天' },
17
17
  ];
18
+ const 读成base64 = (f) => new Promise((res, rej) => { const r = new FileReader(); r.onload = () => res(String(r.result).split(',')[1] || ''); r.onerror = rej; r.readAsDataURL(f); });
19
+ const 选文件 = (dir, accept) => new Promise((resolve) => {
20
+ const el = document.createElement('input');
21
+ el.type = 'file'; el.multiple = true;
22
+ if (dir) el.webkitdirectory = true; else if (accept) el.accept = accept;
23
+ el.style.display = 'none';
24
+ el.onchange = async () => {
25
+ const files = [...(el.files || [])];
26
+ if (!files.length) return resolve(null);
27
+ try {
28
+ const payload = [];
29
+ for (const f of files) payload.push({ name: (dir ? (f.webkitRelativePath || f.name) : f.name), data: await 读成base64(f) });
30
+ const r = await fetch(API + '/upload', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ files: payload }) });
31
+ const j = await r.json();
32
+ if (!j.ok) { alert('上传失败:' + (j.error || '未知错误')); return resolve(null); }
33
+ resolve(dir ? j.dir : j.path);
34
+ } catch (e) { resolve(null); }
35
+ };
36
+ el.click();
37
+ });
18
38
  const Ico = ({ size = 18 } = {}) => h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round' }, [
19
39
  h('path', { key: 'a', d: 'M5 3h9l5 5v13H5z' }), h('path', { key: 'b', d: 'M14 3v5h5' }), h('path', { key: 'c', d: 'M8 13h8M8 17h6' }),
20
40
  ]);
21
- const CSS = `
22
- .gn-root{position:relative;display:flex;flex-direction:column;gap:14px;padding:18px 22px 52px;height:100%;overflow:auto}
23
- .gn-head{display:flex;align-items:center;gap:10px}
24
- .gn-title{font-size:16px;font-weight:600}
25
- .gn-sub{color:var(--dsw-alias-text-secondary,#8b98a8);font-size:12px}
26
- .gn-ver{margin-left:8px;font-size:11px;opacity:.75;border: 48px solid var(--dsw-alias-border-l1,rgba(255,255,255,.12));border-radius:6px;padding:0 5px}
27
- .gn-card{border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.08));border-radius:12px;background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.03));padding:14px;display:flex;flex-direction:column;gap:10px}
28
- .gn-row{display:flex;gap:8px;align-items:center}
29
- .gn-label{font-size:12px;color:var(--dsw-alias-text-secondary,#8b98a8);width:92px;flex:none}
30
- .gn-input,.gn-select{flex:1;min-width:0;padding:7px 9px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.12));background:var(--dsw-alias-bg-base,transparent);color:inherit;font-size:13px;font-family:inherit}
31
- .gn-btn{padding:7px 16px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.14));background:var(--dsw-alias-brand-primary,#2f6f9f);color:#fff;font-size:13px;cursor:pointer}
32
- .gn-btn[disabled]{opacity:.55;cursor:default}
33
- .gn-out{white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,Menlo,monospace;font-size:12px;line-height:1.65;max-height:420px;overflow:auto;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.18));border-radius:8px;padding:10px}
34
- .gn-ok{color:var(--dsw-alias-state-success-primary,#46b47a)}.gn-err{color:var(--dsw-alias-state-error-primary,#e0645a)}
35
- .gn-tip{font-size:12px;color:var(--dsw-alias-text-secondary,#8b98a8)}
36
- .gn-badge{position:absolute;right:16px;bottom:12px;display:inline-flex;opacity:.9}
37
- .gn-badge img{width:121px;height:20px;display:block}
41
+ const CSS = `.gn-root.cd-root{position:relative;display:flex;flex-direction:column;gap:14px;padding:18px 22px 52px;height:100%;overflow:auto}.cd-root .gn-head{display:flex;align-items:center;gap:10px}.cd-root .gn-title{font-size:16px;font-weight:600}.cd-root .gn-sub{color:var(--dsw-alias-text-secondary,#8b98a8);font-size:12px}.cd-root .gn-ver{margin-left:8px;font-size:11px;opacity:.75;border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.12));border-radius:6px;padding:0 5px}.cd-root .gn-card{border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.08));border-radius:12px;background:var(--dsw-alias-bg-layer-1,rgba(255,255,255,.03));padding:14px;display:flex;flex-direction:column;gap:10px}.cd-root .gn-row{display:flex;gap:8px;align-items:center}.cd-root .gn-label{font-size:12px;color:var(--dsw-alias-text-secondary,#8b98a8);width:92px;flex:none}.cd-root .gn-input,.cd-root .gn-select{flex:1;min-width:0;padding:7px 9px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.12));background:var(--dsw-alias-bg-base,transparent);color:inherit;font-size:13px;font-family:inherit}.cd-root .gn-btn{padding:7px 16px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1,rgba(255,255,255,.14));background:var(--dsw-alias-brand-primary,#2f6f9f);color:#fff;font-size:13px;cursor:pointer}.cd-root .gn-btn[disabled]{opacity:.55;cursor:default}.cd-root .gn-out{white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,Menlo,monospace;font-size:12px;line-height:1.65;max-height:420px;overflow:auto;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.18));border-radius:8px;padding:10px}.cd-root .gn-ok{color:var(--dsw-alias-state-success-primary,#46b47a)}.cd-root .gn-err{color:var(--dsw-alias-state-error-primary,#e0645a)}.cd-root .gn-tip{font-size:12px;color:var(--dsw-alias-text-secondary,#8b98a8)}.cd-root .gn-badge{position:absolute;right:16px;bottom:12px;display:inline-flex;opacity:.9}.cd-root .gn-badge img{width:121px;height:20px;display:block}
42
+ .cd-root .gn-ghost{background:transparent!important;color:var(--acc)!important;border:1px solid var(--acc)!important}/* ── 视觉区分(2026-10-29):每个法律插件一套主色/圆角/头部处理,避免同一张脸 ── */
43
+ .gn-root.cd-root{--acc:#F59E0B;--acc2:#FCD34D;--tint:rgba(245,158,11,.10);--radius:10px}.cd-root .gn-head>svg{width:30px!important;height:30px!important;padding:6px;box-sizing:border-box;border-radius:10px;background:var(--tint);color:var(--acc)}.cd-root .gn-ver{border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.12))!important;background:var(--tint);color:var(--acc);opacity:1}.cd-root .gn-card{border-left:3px solid var(--acc);border-radius:var(--radius)}.cd-root .gn-btn{background:linear-gradient(96deg,var(--acc),var(--acc2));border-color:transparent;color:#fff}.cd-root .gn-btn:hover{filter:brightness(1.06)}.cd-root .gn-card{border-left:0;border-top:3px solid var(--acc);border-radius:var(--radius)}.cd-root .gn-out{font-family:inherit;font-size:13px;background:var(--tint)}
38
44
  `;
39
45
  function 面板() {
40
46
  const [profile, 设profile] = react.useState(PROFILES[0]);
@@ -54,7 +60,7 @@ window.__ModuleLoader__.load({
54
60
  设结果({ ok: false, text: '面板后端未连接:' + String(e) + '(插件宿主未注册出口时,改用 CLI)' });
55
61
  } finally { 设跑(false); }
56
62
  };
57
- return h('div', { className: 'gn-root' }, [
63
+ return h('div', { className: 'gn-root cd-root' }, [
58
64
  h('style', { key: 'c' }, CSS),
59
65
  h('div', { key: 'h', className: 'gn-head' }, [
60
66
  h(Ico, { key: 'i' }),
@@ -72,6 +78,10 @@ window.__ModuleLoader__.load({
72
78
  h('span', { key: 'l', className: 'gn-label' }, f.label),
73
79
  h('input', { key: 'i', className: 'gn-input', placeholder: f.ph || '', value: values[f.k], onChange: (e) => 设values({ ...values, [f.k]: e.target.value }) }),
74
80
  ])),
81
+ h('div', { key: 'pick', className: 'gn-row' }, [
82
+ h('span', { key: 'l', className: 'gn-label' }, ''),
83
+ h('button', { key: 'f', type: 'button', className: 'gn-btn gn-ghost', onClick: async () => { const p = await 选文件(false, '.json,.txt,.md'); if (p) 设values({ ...values, input: p }); } }, '选择文件'),
84
+ ]),
75
85
  h('div', { key: 'a', className: 'gn-row', style: { justifyContent: 'flex-end' } }, [
76
86
  h('button', { key: 'b', className: 'gn-btn', disabled: 跑, onClick: 执行 }, 跑 ? '生成中…' : '生成三份文档'),
77
87
  ]),
package/lib/run.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  * CLI 行为保持不变。
5
5
  */
6
6
  import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
7
- import { join, resolve } from 'node:path';
7
+ import { dirname, join, resolve } from 'node:path';
8
8
  import { buildDocuments, buildModel, DISCLAIMER, InputError, NO_HIT_NOTE, PROFILES } from './docs.mjs';
9
9
 
10
10
  function todayLocal() {
@@ -26,13 +26,13 @@ function isFile(p) {
26
26
 
27
27
  export function runPanel(opts = {}) {
28
28
  const input = String(opts.input || '').trim();
29
- const out = String(opts.out || '').trim();
29
+ let out = String(opts.out || '').trim();
30
30
  const profile = String(opts.profile || '通用').trim() || '通用';
31
31
  const org = String(opts.org || '').trim();
32
32
  const date = String(opts.date || '').trim() || todayLocal();
33
33
 
34
34
  if (!input) return { ok: false, text: '请填写报告 JSON 路径(扫描报告或 dsh-fix-verify 的 --json 输出)' };
35
- if (!out) return { ok: false, text: '请填写输出目录' };
35
+ if (!out) out = join(dirname(String(input)), '合规文书');
36
36
  if (!PROFILES.includes(profile)) return { ok: false, text: `文档档次只支持:${PROFILES.join(' | ')}` };
37
37
  if (!validDate(date)) return { ok: false, text: `日期不是合法 YYYY-MM-DD:${date}` };
38
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepwhale-cn/dsh-compliance-doc",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "一份能交出去的自查表、整改说明和合规证明",
5
5
  "type": "module",
6
6
  "license": "MIT",