@deepwhale-cn/dsh-cn-doc-formatter 0.1.2 → 0.2.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.
package/README.md CHANGED
@@ -118,6 +118,12 @@ pip install python-docx reportlab fontTools pypdf
118
118
 
119
119
  ## 用法
120
120
 
121
+ ### 面板(v0.2.0 起已带面板)
122
+
123
+ 侧栏「深鲸公文排版」:选 .md/.txt 源文件、填输出目录(可选标题 / 发文机关 / 成文日期)→ 勾选 docx / pdf / txt → 点「开始排版」→ 面板直接显示产物名、体积与绝对路径。面板走宿主 `/dsh-cn-doc-formatter/api/run`,复用随包的 python 排版脚本;Python 或依赖缺失时显示可读错误,不白屏。
124
+
125
+ ### CLI / 技能命令
126
+
121
127
  技能正文里有完整命令。核心三条:
122
128
 
123
129
  ```bash
package/lib/api.js ADDED
@@ -0,0 +1,42 @@
1
+ /** dsh-cn-doc-formatter · 宿主出口(面板后端) */
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { readFileSync } from 'node:fs';
5
+ import { runPanel } from './run.mjs';
6
+
7
+ const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
8
+ const ROUTE = '/dsh-cn-doc-formatter';
9
+ const VERSION = (() => {
10
+ try { return JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version || ''; }
11
+ catch { return ''; }
12
+ })();
13
+
14
+ const 回 = (res, code, obj) => {
15
+ const body = JSON.stringify(obj);
16
+ res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'content-length': Buffer.byteLength(body) });
17
+ res.end(body);
18
+ };
19
+ const 读体 = async (req) => {
20
+ const chunks = [];
21
+ for await (const x of req) chunks.push(x);
22
+ const text = Buffer.concat(chunks).toString('utf8');
23
+ if (!text) return {};
24
+ try { return JSON.parse(text); } catch { return {}; }
25
+ };
26
+
27
+ export function registerApi(ctx) {
28
+ const handler = async (req, res) => {
29
+ try {
30
+ const u = new URL(req.url || '/', 'http://127.0.0.1');
31
+ if (u.pathname === `${ROUTE}/api/info`) return 回(res, 200, { ok: true, version: VERSION });
32
+ if (u.pathname === `${ROUTE}/api/run` && req.method === 'POST') {
33
+ const body = await 读体(req);
34
+ return 回(res, 200, await runPanel(body));
35
+ }
36
+ return 回(res, 404, { ok: false, error: 'not found' });
37
+ } catch (e) {
38
+ return 回(res, 500, { ok: false, text: e?.message || String(e) });
39
+ }
40
+ };
41
+ ctx.webServer.register({ kind: 'prefix', path: ROUTE, handler });
42
+ }
package/lib/client.js ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * 深鲸公文排版 —— 面板(选 .md + 输出目录 → 生成 docx / pdf / txt)
3
+ */
4
+ window.__ModuleLoader__.load({
5
+ id: '@deepwhale-cn/dsh-cn-doc-formatter',
6
+ factory: (require) => {
7
+ var module = { exports: {} }; var exports = module.exports;
8
+ const react = require('react'); const h = react.createElement;
9
+ const SLOT_ICON = 'sidebar.panellist'; const SLOT_MAIN = 'main'; const PANEL_ID = 'cn-doc-formatter';
10
+ const API = '/dsh-cn-doc-formatter/api'; const BADGE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAtYAAAB4CAYAAADWmbVxAABH9ElEQVR4nO2dBZQTSReFH4O7u7u7w+Lu7jDAoovbj+4u7u7u7m6Duy7uvrgs7v6fW6GGTFKdpJPMTGd4354cZqPdnaRz69Wt+4KRk8RKkum7s49lGIZhGIaxxeN83nyAmMBjUZdgzjxM14NYTDMMwzAMExCwsGY8UWQ7dEcW1AzDMAzDBCQsrBlPFNhe9u7AopphGIZhGIb55ak74rtLwppFNcMwDMMwDMM4Jq41hTWLaoZhGIZhGIZxXFwrhTWLaoZhGIZhGIbRJ66thDWLaoZhGIZhGIbRL679CGsW1QzDMAzDMAzjnLj2FdYsqhmGYRiGYRjGeXFtN26PYRiGYRiGYRj7CGHN1WqGYRiGYRiGca1qzRVrhmEYhmEYhnEDLKwZhmEYhmEYxg0EYxsIwzAMwzBG43E+78DeBIbRDVesGYZhGIZhGMYNsLBmGIZhGIZhGDfAwpphGIZhGIZh3AALa4ZhGIZhGIYJ6sI6eHAvcQkWLJif60KGDCn+7t65NV0/d5DKlirq5/4hggcXF/PHMQzDMAzDMIx/EoIMzNev3/z8v5eXl7gOF4jm3/Lmogjhw1OKZEnE/3t5BafPnz8H2vYyDMMwDMMwvy6GFNYQ0Cg2d+3QimLGjE59Bo6kN2/f0bdv3yh3jqwUOnRo2nvgMLXu1Ivy581JK9dsou/fvwtRXaJoASpSKD+9ef2Wps1eSP89fSZEN243Mqi0Ww4kGIZhGIZhGM8hhBFFNQR0nNgxqUObZuK6rdv30Nbtu+mv7h2oTYvG4rrpcxbRn32H0r+374r/hz1kSL8eVL92Nd/nSp8uNdVv0tYjhLWRRbUnHD+GYRiGYZjAxjAea3iipajG38+ev6RBI8bTmvVbaP/Bo5Q2dUohqnE7RGizRnUpZfKkFDJECCGqF8wcL0T1l69f6dOnT6J6nS5NSvLyCiYeY3S/dZZM6X2940aDRTXDMAzDMIwHCWsIYimApTgeO3EGtWjXjd6+e0epUyUXAu/bj8op/k6RPCl9/vKFunZsRYUL5BViOjhsJF6mBY4z5y4RIhw2CyOKQwwkQLy4cWjz6gWUM1tmsf/YXiOAAc6KBdMoe9ZMgb0pDMMwDMMwhsfLKAKuWuVylDF9GpNgTpaE/tfhDypfprioSJuLUIDiMwTo6zdvKH68uNS6eaMfAjq4uB2PGTV+Gk2YOls897dv38X9jVa19vqxPblzZhX7h3QT7H8wCmYAj3swSpokERXIn5tqV68UqNvDMAzDMAzjCQSqxxqiF9XpcmWK06TRg+jps+dUqFQ1Wr5wGsWLE1vcB4sU6zdpRzdv3TYlf5iJ48dP/hOWEFR4UbkOGTyEqHpjUePKNRspYoTw9PrNWzI6aVOlEIK6RtUKNHHaHHrw8HGgLmZE1R/Hs2K5kuJ4YsDDMAzDMAzDGLhiDVsHxPLZcxfp6rUbtH3XPnrz9i09ffrc9z4F8+ehedPH0uvXb4RIlpVrCNG8ubJT1Uplxd9ewbyEfQSi+uvXr7R7ywo6uHMdDRvwp3gM7BaxYsYQjzVK5fo7mewpiRMlENsUJXIkGjWkj7gOojowsrhR7Yeojh0rBjVtVFccu0QJ4wfoNjAMwzAMw3giwWIlyWQI8zE80eYZ1H8086aqFctS2tQpxG3Pn7+g8OHDUahQoXzvI6rUIUxVagjQtRu2UsSIEahY4d/8PHeGnEVp0phB9OrVG2rSqrNvpTywkVXpaROGU6VyJcXAAPu3bpMPdenRj16+eu17PwCbOAYR7vSLS4sMZgK+fvsmnhuJLHOmjqGsmTOYfO3fvlO8FFnd9poMwzAMY4/H+bz5IDEehyHi9iDsIKrxL0Rv3VpVKEH8eHToyHGKET0qxY4Vi6JGjWL1OIhqUa328hJCGX7g6NGiir/xPGDT1p1ClHfq3tfXWgEBaSRkUdoreHCxjRXLlqRM6dPSuMmzaP0mH3r1+o2f+4tulPRTCDsKjpO00qBajteSQl0ekUrlS1HfXl0obpxYvgs/v30P/EEIwzAMwzCM0TGMsIavF2JvQO9u1Kh+Tav7iEV9FrYI8+sgpCGq5d8r1mykOQuW0bF/Tonr7ty97+dxRuLTp89WVewkiRPSqCG9xSLOXXsO0L5DR+ns+UsitxuVbfP7o6Jsa59MqSgkKvuWQ4pw4cKKBaD58+SgWtUqUrYsGcX1uC+iCsHbt+/cvs8MwzAMwzBBDUMIa3PBhzxn8O7dewoZMgSFCBFCM9HD/DpZXT1/8TJ16dmfTpw6K54rWtQo9Oz5C1GtdbeNwlVk+gcWZppjEsuoJpOoHKOCjwuuu3vvAZ27cFks6ty4ZYdYwAlkBrg5cp9lpR5iHfaO9GlTUaoUyYTvPGaM6BQrZnRf77q01cjnw9/mgxKGYRiGYRjGgMJaijdUSQf360Fbt+2mLj37CX9vgvhxfe9nqp5qr7PE7RCjp89eoBr1m9OXL19pn89qSpUymVgUWaJiHfrw4SMZlctXrot/zWP2zBdpwvKB27CPWEiIC6L5kN89f/FKGjF2iqhim4tr878rlClB9WpXpXy5s4t28Cpgn4FNxPw4S0/3mXMX/HX/GYZhGIZhggKBmgoi/b4N6lSnLBnT0x/NGgq7Q9GyNah1x560fPUGzWqsRF7/8NETatC0rVjwFzp0KNqz/5D4++3794ZtFy693sdOnKL37z8oG9lI37m8TXaehBBGNb59qya0duksUdmWAxB5vBImiEcLZ02gGZNGUJGC+YSolo/Fv2Km4IdPW3a+9Pvaptffs/9wgB4XhmEYhmEYTyRQU0Eg2iDqEO3WrlVT2rPvkIjcMxfReXJmo6EDelGaVCl87R7myOvQoRHtz2VcHEC8HvzB6NwoX8toSE81BDDSTCC25cJLe2B/IJKxz6jMl6vmLSIJcfySJ01MqxbPFAkfuA8q3vBMOxrfJ48VssXzF6tEL16+cmk/GYZhGEYPnArCeCKBWrGW4u3R4/+oV58h5LNjj2+VdO60MdSnZ2c6fOwEVa3TVPiKpffYUlTDTw1RLRvFyLbg8B9DVJu/llGZt2iFyUuuo+si7i8GEp8/U8oUyahzuxbi+ESOFJHmTh8rRDWOh6x468nEFvaTYMGEj5tFNcMwDMMwjIfkWJungsiEi+xZM4lq89XrN4SAxkK77RuWUNQokcVjZMQeRGP7//1NS1astcqnlkLS6KLa1EKcaOPK+WJxoaoybwu5KBPpIumyF6ZO7ZpTmxaNfXO+9SKzq798+UwFSlal23fuGf4YMgzDMEGLoFqxntOyNDUsYApqkHz8/JXCNBpDQZHDfetS7hQ/182Byw+eUZousykoEqgVa0tLg8xVBv+cPEOXrlwT10Ec3n/wkP7sN+xH0oXpMRDSSA/ZtfeAMp/aaCkgWkBUYz+7/TXQd2CgZ7vlACJMmNDUpGFtql65vKhcO2opsQTHEcJ+zMQZIt4Pgx6GYRiGYRjGNoZVTHIRHkDlFUJv1dpNdOT4SeEV/vLDRw2rCKwkMlrOE5EVaqSa9Bk4UvwNga23+Qvu3/aPJsKzbqqC62+HLmcBYK8ZN2mmyQNusIY6DMMwDMMwRsSwwlomVlgyevw0PwsRkVttnjDiqUBcQ9BOn72QRo6b+qNVuykFxFFwXCJFjOCUoDYX1f89fUbN23b9Ie6Nb6VhGIZhGIYxAoYV1irhiSrs/oNHRe5zyJAhxfW3/r1LQQUIWVSIh42eRP2HjBF/Sy+5ozgjgvEYucjxyX9PqWaDlqIpjOViUYZhGIZhGCYICGtZlYYAXLpynW9V9satf8W/WPgYFJC2kAlTZ1P9Jm2FwIXglRnW9tBbrcbryXQR+Nor1mgkZgFkDCDDMAzDMAwTBIU1xDMquD4794qIOdMCxhA/mpuYGpxIb7GzdggjiettO/dS8Qq1acqM+cIWgn1zh9iVi0UBXufjx480avw0Iapv3LrNopphGIZhGCYoCWsIYwhJiGbZFVB2C0QzlMtXb4j7vH7zRohECG3zToK4yK6FMsfZVlt0o4rrFy9eUu+BI6hSrcZiv12pJMuujfK44O+VazZSyYp1aeioiaa25m4S7wzD/Bqgo+ujm6etLueO7QzwbUHvA9W2NPGuE+DbwjDMr4n+kGN/F9OmJikyFcNc4sGuEC1aFIoaJQpt37lX+IFTpUhG8eLGptev39KLly9Fa/D/nj2nN2/eCrGt8icLa4VZZrZRkSIYcXfH/jlFFWs2pvkzxlGObJl925freS6Zjf3g4WPasHmbyP5G4x1g8lPrWyzJMAzDMAzDGExYCwtHMJOY/vr1u6/4RTfBTBnSUsb0aShdmlQUP14cih49GkWMEN63bXmRgvn8PBeE8stXr+nVq9f08NETun33Hl25doMuXLxMFy9fo7v3HvgR2/K1ESlnRJEtbRs4Hs+ev6DaDf+gtctmi+PhiLiW1XsIZyz6nDF3Ea3b6OPbTRHXY7e5Ss0wDMMwDOPBwlp2HBQWjx8NTvLlzkGlSxSh/HlzUvKkiTW90rKjoBSEshU6LlEiRxKXRAnjU64cWXwfAy/xlWs3RfV3975DYrEeouVkjVZWdI1YyZbi+vWbt9SxWx/asmah3cdIUY3jPHHaHBo6apI4BgDP9e37dxbUDMMwDMMwniyszQU1SJk8KdWsVpEqli1BSRIn9HNf0Y2Rvgt7iLl4lli2/paC2CQqf6aF4PHwAqL6jcvv3rVFZRvieuOWHbR9115R4ZYID7LBqthSXKORzJZtu6lsqaK+12m1Jcfx6dyjHy1YsvKnt/rbN10RfgzDMAzD+CVpzMhUK29qyp8qPmVMGIOiRwhLoUMGpxdvP9KlB09pz8W7tPDARbp0/xkfOgcI7hWMSmdOSuWzJqMcSeNQsliRKVLYUPT56zd6/vYDXXn4nI5ef0jr/rlOB67cI6MS4MLafPEdBG7Lpt5CUIcKFUpcJxYgosr6Q0BbCmd7SNGtqnTLCq58/siRIlLRQvnFBdaIPfsOiSi/vfsP+1pNpFXCSN5j7NuGLduFsMaAQYUU1T16DxaiGhV+iGkW1AzDMAzjPBkSxKBhdQtS6UxJRcHPkpiRwlLMSAmoQOoE1KtSHlp86CK1mbNTiEM9LGtXgWrkTmV1fbIOM+jmk5e6t/vfcc0oUfRIVten6TKbLj/QL/7vTmhB8aNG8HPd2C0nqMP8XbqeB8ewccEM1KdaPkoYPaLV7SGCe1HYUBEoXtQIVDhtQupaPiedvv2Eei7dR5tO3SSj4RXQKR8Q1QkTxKMxw/rS1rWLqXrlckJUQ/BJz7BMAXF3ZJ550ohsAY7twQXWkUrlS9Gi2RNpx8Zl1KpZQ4oWNYpvEolRUkVkBf7mrdviX9UhkgsVt27fTbPmLRGiGgMFI1XfGYZhGMaTQEGuX/X8dGqwN5XJrBbVluA+dfOlpVODG1C+VPF0vd6Gk9eV1xfPkJj0kiJ2FKWoBoXSJtD9fKjQW4pqsO6Eepu1iB05HG3vUYNmNi+lFNVaZE4Ukzb+rypNa1pCCG8jESBbY6r6mhInGtSpTtvXL6E6NSr7Vq9NedQmsRuQyIq43D4polOnSk69e3aivT6rqN+fXYRVxSgC2yuYaVtxHIGlVjZ5qk2NdAYOHyf28dt341TbGYZhGMbTCBUiOK3pVIn+qpJHWBb0AlELAZkreRyHH4NqLGbYLSmeIZHu1y9m4zGoAusFAwtLXrz7SHsvOd4NO3nsKHSob10qml7//kiaFclEi9uUc2iQE1D4u0KUeclxYsekOVNH04hBf1GUKJH9NCgxQjMXKbIhmmXec8wY0alFkwa0c9MymjZ+GGVIlzpQBbasPDesV4Pq1qzsJ0LP3AKCfTl2/JRIAYHI5sQPhmEYhnG+Uj3/jzJUIVtylw5h2FAhaF3nKpqVY0v+e/2eDl99YHU9hKhe2VQsvXaV25mKdZks1sJ686mb9MXBPhiRw4ambT2qC5+6q1TPlYralsxGv4THGqIPArpA/tw0bng/ihc3jvh/5DKrFtwZBSmYRRX72zdhVYFNBJ7mtRt9aOLUOXTh0hU/+c/+bbOQorpE0YI0pF9PIZYhmrWsInsOHDYt9NTwYDMMwzAMY5/2pbNRzTypNW/ffu5fmrvvvBDB956/oYhhQlHSWJGpas6U1LBAemF3kOBv8/+3x/qT160sJDEihqUsiWPRyVuPHXoOiHBbVWl4l1PGiUpXHz536Pmwf1iw6YoNJE6U8FbXvf34mebvv0Crj12j07cf07M3Hyhi2FCUPFYUIeTblswq9l1Fn2p5ac7ec/Tq/ScKbLz8s/oL8deofk1aNm+Kr6iGoDZChdoRZIqGtImEDBlSeMJ91i2ikYP/psSJEvhaWfyrgi23QYrq2VNG/WykoziOGFmDfQeOmBZr/hDaDMMwDMPoAxXVATXza1aUq45eRyUGr6AF+y/StUcv6P2nL/T41Ts6cu0BdVu8l9J0mSVEt7NsOHHDZZ91poQxxYJKW+ipWuO1Q1rMliO5Y/Np5xcSLjp4USzK/GPWdvI5e4sevXwnnhPi+tiNh9Rv1SFK0WkmbTl9S/n4qOHDUJUcKckIuF0Jyjg8CM6e/2tHQ/v3EtfDPmHkKrUjAwVzgV2/djVhEYEXO1bMGH4sIu6wt5iLegxIMECZO22seG15uyUysxqNZGADAaimMwzDMAyjn56VclO4UKbfXXMg+IoNWk6rj1+1+Xj4jhtN2UJD1h116vCfu/ufMgFEj8/alr/aGZ91mSxJrK6Dt/rlO1OfDL1gAFJv4iYxILEFnr/SqDV05vbPaGRzymdLRkHSCiIWy337JrzUWGAnrR+eUqW2L7CD+WZERwgfXqSH1KleiRYtX0NzFiyj23fuOd06XQ5KZBdKXKJGjUJ9e3WmWtUq+sYFah1LU8ReMNFp8s3bt75+cU8FSS2VK5QWcYjp06am6NGiikELohGROX7qzHk6cPgYbfbZ5dv4xr/AMU+bOqWwNeXPk4MSxI9L0aJGpejRooj35M3bd3Tn3n26cvUGHT56grbt3EuPn/znr9sUlEiRLAkVLfybaOiEv+PGiU3hw4cTq3PRFOnO3ft0+ep1Onj4uEi7weAxoChXuhjVqVGFsmfNSBEjRqAXL17SxctXafdeUzwnmkwFhf1FN1fY3bJnzUSpUiSjKFEiiex/7C8+1/sPHaVlq9aL7rVBATQkq1CmhGhIhg6/WFMTIoSpCdetf+/QmXMXaeeeA+Ly+fPnwN5cJhCAXcG7QDrlbbXGb9AUeCp6LN1HiWNEojr50ujejo0nb1Cbkln9XIcoP2Rmf/xsvydF0XR+hTXkiKWM0FOxRsygJciWdoYZu87SsA3HHL7/py9fqeOC3bSjZw2r23Imc3xhqEcJawi5scP7Ue3qlYR9QXZIdDfS/wwCWrhbCmyI39bNG4mqMkTe4uVr6NCR48rW6VpAgEtbCfYqXLiwQky3bdmY4seL6+uptrWf0vZx6rRp2gmvZzRZ3aNzG+rQppmf654/f0FpshXyc6xaNmlAndq1EO3rLcEMAS74MfSuW100+pm3aDmNmTBDDCjcCWYIalQtT21b/k7JkmiP+iFAIPyzZExPNatWEO/Xrr0HaNykmXTk+EmHX2/K2CFUpWIZq+tHjptKw0ZPcmofEG2JFB5L0BypbNUGTj0nng/Pa0nP3kNo5rzFDj0HPstlShYRxzZbloya90PsJS6ZM6YTxxbnlU1bdtDI8VN9Z2acYXDfHqJRlDlovlSyYh3fgd3U8cOocIG8fu4DAYZLwfx5qEeXNjRn4XIaMWayGPAZeX+1KFb4N+raqZX47KqQ+4tuuJ3bt6T5i1ZQ/yFj6O0729UlI4PF3z26tBXnEa3jj/cI5/Snz57T7PlLafL0ebrOL0vnTbH67IAdu/dT3catyV2sXTab8uS0XriF7za+44zz1M6TRqSBqGwL8FXrBfnO8ApHCRda1+PWn7AW1lgImS9lPNp14Y7NxyKKrqCFaD549R6ljRedokUI43tdgmgRRUrH9Ucv7GZ4q2Lx1p24Rnp5/eETdVuyV/fjdl24TbeevKIkMf0uAsWiULxfEN9BygoC7zFENUSlf4pqaZUITM+2pUUkfLhwwoO9cuF0kYX9V/cO4scIjWgw4JBVaNUFj4c4w49pl/Ythc0EixSlqHbEXiIXKh4/cZo8FQwoFswcLyw2KlGtAscXYuXQrnVUomgBt20LKuVH92yg0UP62BTVKvB+FS9SgNYtn0PTJgwXwsQRtu7Yo7y+0G95yFkK5s+tvD5zpvTiM+sMRQurPYdbtu926PHJkyamNUtn0ewpo22KTBU4r2Ax8a5NK+jvHh19m0u5k7Bhw9DiOZOUwsjPtoQMSc0a1RXb42n7C/G4YOYEkd2vJaotwfm2cYNatGHlXDFr42ngHDt94nAaPbSvUlSrwIAZ5+TDu9eLdS6Oguq+isIF8jn82vbA2qXcOfwKLnDj1m0W1W5A1ZwFDF3veIXVHFgdxm05oftxuy/eoTcfPjvls0a8HxYbmrPz/B06fvOh1X0LpUngVBoIKvcQunpZeuiysNToBRX3Q1fvW10PiRQvqmO6waOENbzH/umnlqIalYPpcxbR3IXL6dMn0yrQwGqAYimw8S9sA21aNKZVi2bQgR1rxY/qsAF/iuuqVS4nLA641K1Vhbp1ak0zJ42k/dtWk8+6xfS/Dn9Q0sSJ/CyMtIe8HywRskqhyr80+o/ewpkTRAXNGfBjNW/6OHGMXQFCBoMiCA78cLlKpXIlxUArR7bMdu+7c/d+ZXfMLJkzODzQMAefIwzOVOA7CiuCXvA5K/SbteA8d+Ey3btv3yZQpmRR2rZhibLKpnc7MFO0dukstwkVCQa1jgpgnH/WbtjqUfuL89O29UucHojCNoJzFSxangIGQQtnjqeKZUs69XgMjjHob9+qiUP337R1h7LCjfexWuWy5A6qVSqrLLgs1xD1jONECBOScqewPndeuPdUlwXEkkUHL+l+DCqwWNDnjLAupsiIRsX32PVHVtcXcsBnrcqv1tsURuJK10S8D1qLGIOkFcS3q6GFv1eEvwWz3XbcFlI4w29YunI9+ve2KYgcU+0TRw206T8OSIuIZWt2OZWaN1d2h54HwspLZzt3ue8Xr1yj+w8f+XrdPQmImXx5crj0HPjsQRR/+vyZps1aoPvxqAYib91Zca9F7FgxaMWCadSweQfas/+Q5v1gazl6/CTly53DSgTny5NT+G31UCB/Lpu34zl37T2o6zlzZssiZgkscWTbYG2AVcydCToQwBi4Vq71u1t87fDcYtbNUTZv261pAzHi/iZJnJCWL5jq8CyKrUrurMkjqXz1huQJYDYVayRcBYvyv3z5ShOnzbF5v/fvP9D6TduUNizY/GAtcZWqlcoofwtWrNno8nP/6uROHlfZCAZ+Z1dA63AsSISlQg8bTt4Q8X3mZE8aW9hKsEBSC8vmKx8+f6GDV+9TpLDWdpTC6RLYHWzkV3SPdNZf/c9Na3HvKFr7HCakv6ZIB07FWv6AmFs15EVG0pknh0gbhCMiEEIdj1u5dpOo3sF2gSnblWs20vMXL30FvRFQtU63tH6YX2d+DJzpQgkRj9c59s9pX9+5J4GqLnyP7gIdM50Rx5PHDHa7qJbgszp32hjhDbeFj5YdpIB+O4g9IYGFmHrBojsVW7btsvk4HFd3i0xzqwWsG1iQ5grhwoahgb276XrMspXrPGZ/cf+Fsya4LKrNRbrKa280qlQsLcSsu8DgXTVr46gdBDMGaDjmCmlSpRAzB5ag0GS+iJ5xjsyJYyqvP3PH+Wq15MRNx/KnLQW95Sw0hH/hdNpVZviw86bwK4QPXrkvFjwev2FtBYFH2dK3bNlkxtJz/uDFW6WtxB5oJHP32Wtyljcf1HnVoUIEvvZxu7THimqIXlRwRoydIha4xI8bh+LGiUVxYseieHFji5N6jOjRlBVZYX8gU6XX8gdJ2ktqVCkvfI2g2Znz1P3vQRTCn/zc7q5k+9triNmAYLRnn77qoxEIFToUDVCIGQw4MJ26cs0m0ZAHSSDRokamZEkSU7kyxcXACgvMVOBYjBj8NxUsUUV8Jh0BXTbLlylu8z7YhnUbt5LPzr0iOeDxk6cUNkxoihUrhrB6lC1ZjIoVzq8ppqS4LlSqGr16/UZ5H5/te6hPz85W12OxnB5wDH7La7tiDZ81fO3v3r13+HlVAw/MkiBFQQvYUSaPHWJTZGKR2PJVG0T6BFI3MDOFwSLOFymTJ6ViRX4T0/haohBCZVCf7tSpu/NCL2UK67imjVt20Kz5S8Si4C9fv4jPX6nihel371ridixS9ZT97dmlrUgh0QLn300+O2jt+q109sIlevjoMYUNG5bix40tvMHVq5QTotAcrCUwOjmzW1ue8Jlfs2Gr+D5fuXaTnvz3VNf5ZczwvpS/WCWb351DR/4RCS8JE1hX+SD0z10Y7vQ+wVKoYtlKtoG4g2SxoiivP3dHbUHQw1knxDn82ceuP7Syp8AOsua4euHgb6nji+QQc3ZeuC3+RSMbiOK4Fo1aELs358l5h20g609cF55nvfz3+r2/2FWN0BTP7Wr09p27wneHqKjpsxcq7xMpYgSKGzc2pUmZnDJlTEcZ06ehVCmSC/FtLrZRwcVxt0zDkCc7/AhkyZSeli+Y5us/DQqxfo4iM67PnL0gIrKQrnHkmCmBwtKGY2SwgM5yEd3Z85eoQ9e/hW/XUtjicvDIcRo9fpoQFloLx+LFiU3tWzelAUPH2t0GeKkhOrSAd33spJli+vfDh49Wt2EgiUiyRUtXi4o0tkv1Yy5fq2+vLtSxex/l7ddv/isWH1kumITYwj5BxDpC+rSpxAI1W2CwisVPjtpBYGlRVdq2brNtA0Gevco+AhBlNnTUJJo+Z6HVsQWovuGCNIWBQ8dR6xaNqF2rJsrF0fVqVaUVqzeKz4c7vl+devQV76k5GOThgvNbzuyZxXnIE/YXn6dmjetp3o7vWsduva0GSNhGnFtw+6Tpc6lhvRr0Z7f2Im7UU8Gx7frnAKvoQPPzy6hxU8XshSqlB+C7iPUcttJ6TLaMDdSxTXOr26pWKkt9B41SrqmwB37nqiq2C+ei9Zt8dD8fYw2i8VQ8eKEuiOgBjWScAV0YrYV1Iodj9oB5isj+y/esFmjCZz1n73mH86ud9Ve///yFgipur5lfuGQKS48SKZJYLIIfbvwgSCsIQKUOsVFoD47YppoNWlK+YhWpRIXa4sQOf9j9Bw9FtUemYciFfEDmOZvaiX9zalGXp4PjgeOya88BYYPB8di2a584tnIhpacCkVeumreVqLYE+cHN23alcZNnad4HCQZaAsccxKZpTavD94zPKCLvVELIEggT+F+1poFB7RqVKHWq5Jq3o2qtoqCOdBBH/aR5LfzcttCyydjyV/+WL5fmIjl4hLFeYvyUWQ4dW8yAQcjUbdRacyYCos8dDBo+zkpUm4OFaaoBiVH3F8k5WhX0Q0f/oYo1GtmcdQA43yJ6rlqdZnbjBY0KFrzX+72N3TxuzCi0bN9dDKi1wEDFXrKOVgUZs7ZFCqnTdeyRK3sWZSrLlu17NGfCGH1ECqtO33n93vVMc2fbbqu6MKaOG01E5TnSGAbJIkev/7Rt7Dj/r8ONYtLFjy6sIpYtyHecM1XA9fIpkCPxPEpYnzx1TvyLqS80zxD+4R9Rc7KyY2rJbfIgSz8xptNwUl+0bA217tiT8hevTPWbtBHiBD9g5gJberTFDhjIVx1QyP3FccFUcu6c2cTxwN+eDvyB3s3a62r4MnDYWFqwZKXyNlTVaisWD5mDhIUqFUprVhdr1m9Bh4/pi0jC571t5z+F/18FPrcQOlr47NQQ1joWXxXIZ31fVbVbj89a5a/G53D/Ie34qQ6tmiqvhwDAgMXeAErF3gOHybtpO2W1GLM3los/9YKB/aRpc516rBH3F4NL5LGrgKUJucp6cqlPnT0vHqPaHiODog0q1Xp+M8QAa9kazdnXmtUq2Hw8Zp+0IlDtPVavDWT5KrXfn9GPqtsirAvvPrkurF87KaxP335Cd55a+5JVVWssasyWJLaf6/Zdviu8zZLtClEMj7WlgNaygSCpBIshGX8W1sdOnBINDeDbxOp1kwj2ex9TY5WfC/fwt7nYhoiG0N62c58QJ4VLVxcJD6jw4DbLk/mvZP8AGKjgOAwZOZESJYwvKvaXrlwTVSc5+PBE8J7j/ZbxiXr4u/8IzQU7qilTc+DZl63iLRk8YoIQEc7S9c+Bvuk1llQsW0Kzmn7k6AlRKXe2Co1Zojy5sll976bPWqjps7YHvpuqBVu2OtNhgI0KrtaACN5iZ8F0/ZyFS5W31XFxIezMuUucmqI36v4i8k/rMw5Lkh6PvQSxnpNnODf4CAwQBdn9r4FOPfbv/sPEYEsr8s4eWrNXpYsXdmhGzfK7jXOHagZv1x7PW2PjabhDbrgizpEO4kjsHhY1Wqaa7DzvV0ijGYwqf1qVDqLKr3Y2DSSo43Zhff7iZbp0+Zr4ES9RpKApBs4BM7m52JZVaYhHXLD446/+w6lExdqiVbS0OvxqlWqAYwOBc/T4KZq3aAU1/72+uH7B4pVC3HhaGog5YyZO1xSh9kC1bfTE6crb4MPXynIG6IanAp+7yTNci8RCNVfL443cbq2EDXwPYPNRVdctF4+pwKDWcor6/MUrtGPPfuUPNaaW7QHPOCp0evzVlcqVUg58b/57W3x+XWXClDnK8wBac7uSpb/v4BGnHmfU/cXtKjAYR9t0Z8ECdUcXBwc2g4aPd3pb8TgcexVYtAxbhy2Qc64qGCDe015zIUsKF8wnuv1asnrdZqcGg4wa2BwsQahC+NDqAaoeIlg0bNHDhpPXHcqqtuevlqg6SFrmWWOfC6SOb1W933jKtejBoIqXfwi/7bv2ih8X+McwFS9j8vQgI+qkyMaPBhaH1W/SViz4kLnNnpbV7Aqmyr5JRDZq0UEsIsJiMpEusGajqVrtoccDg4IFS1a59Bxr1m/RbDmc16J6K4H4xFS6lhfTHZ8vJJtoLTgsUjCf5uOQPOKsz1pV2d5/8ChdvXZTOeWPPGt7qAYB+CHfvttarNvbPyy4c8exRRXx9LkLVtfjvJNVZ4dDCWbGzjth1zDq/uK8oDVw0ooKdBSR1bzR+IvlsPhy3aZtLj3HyrUblRY1kb6jMUshgR9dq6sqss71gLQSFbbWdDD6efVebUdU5T+7y7/tCOiaaCn640QJb5WLbemvfv72A528ZR3zt8Oiiq3yWUO4W8bsofPhk1f6Z7p+BfylvLlq3WZhB0HKB+LLxEJDFyqpMv1CLmbEynQIbHgWcd2vIK5llQr727xNV3r3/j11bmtaaQ5B+uLFS3GMPbWKv3X7HjFAcAVMZ+/YpRZ5uRRtfwESabQWdGE1vzvAZxfVJBVZM2WwmVygsvU44rNWie/9h46I78rpsxec8lkXL2ItrI8eOyk+eypwXLW6F+474FxFWMWhw/8or8+RTT1gsgfWejhT+TPq/iLzWlXhBFvspLk4wsp1m8jobNiy3SmLmaU41lpLkC2z9vfY3iJGzAShQ6ojoBCAqEdLUHSyt/CU0YdWi+6E0fVZd1REc6E7IDzNKjFcMlNiP0Ibiw3N2XPxrjLeDgsYLa9OFiuyH591+WzWMaRsAwlAYQ3hiy/5nn2HhMhr2bSBmGp2R14hRAGEBp4PoqOWd0vhQw3qlWuTTea7+OGGH3L7rn2irW6UKJGFGJ06c75HV6uB3sWBWpw6o/ZDp0ppfWIAadOobRWoMD94qD/EXwutxUvJkyXR9L5CsB79xxSfaA4Wqqmi1yRyfYM5EIqHjpzQPEb2fNZxYsdUNqPYbKMpTOKECTSf8+p151vZOvpcqu11hGtObptR91crfQafcWRnu8rpM+cNP6D/5+RZtzzPWQ3x6shnbefu/cIH7coiRtjWkIVvCVer3Y9WJF4WjcYxesiY0LXnQHa0JaUzJbVpA5H51Zag6qxqelMio0moY5a8fNbkbovZ+xVwe4619FPPmrdENA6AHxRh+5imh53DHR4wVMMhLE6cOku//9GJls+fKkZcgd3S3D+QAwYMWP7Xq7+I/0qcKAE1aWhqkDN11gIhrt11bAMLLUGsF9U0OcAxUxE/Xhyb6Tbu4uRp9fPhfYVohZ9bxbYdeylvrux+roN4g68T/lgVuL+l8Ib4kTaZEwqRgfujVblWu/WiTsTsJUuqXYW7eEI9Le5OkiTS7khmixevnIuRM+r+YoGzCmftLir/MRYOa33HjMCJ0+4R1mfOq4V1ggTaazgsZ65UWeJYQI1YRXsDFFUaCH4jtNKHGOfRareNNuKu4qo4RxdGfFTM5U7BNAlEkgkWRlraQMCu89b+anOfdeZEMa2E9czdZylnsjhWTWSuPnxOl+6rB4mMP1SspWVj976DQvjiRNGxTTMh/L65UfhKcQ3faLe/BprSQjy4YqsC1XlTnCCJ7mpy8VOvru3FIjLksM6cs1jcx9P3/cpV94x+Hz1Sd7SKE0t9IosbR32S1BK6zoLqt1ZaCxYk6m1vbstnrYrZw/dEAkGu+gG3ZQcpXtg6lxlJNLYWm8aO7XplxxUwYHEGZ/OZjbq/aGSiAl0V3cW1G7fIyNy/r7/lsgqtWazYMR1777Uqy0iTsRxAWxI9WlTl9/7AoWMON41iHOfo9QfK9I5K2VNQCEXXaEdBm/ECaVwbhKJjoqXwR4dFmeZhWbFG18Zzd//TfD7VAkb4qiHXKmZLYXUbV6sDwWONNwMiov/QMUJIp0mVghp71xYja3emVkBcQ7BDcKJpAf7GdUEBDFAwWMCPfN3GrWjh0lW+sVkyaqlH78GiConjbfSpWFvgc+GupgZaz4PBh6oBTHiNqftXr62j7lzltcZzqqZ2zaf9kShhSSFbwlrhwd536KewxnQ0RLEl+TSENb5Xqh90e90Wo0aOTIGJlq/YHq8UMYcOvZ5B91erQ6I70zzeGDgZBOeXN28dz+i2hdYxs/UdNgc+aNV3zxE7SOXypZXJL2wD8R8+f/2mbH4SK1I4KqvIdHaUarlSurR40VY6SKlMSShpzMgii9peGog5ey/es2rYEiNiWMqSOBZVzK6wgXDMXsALa4hqiELEOCFmCHTt2Ep0ikJlVWuxmCuZzj37DBHT2Khie7IlQnaYxAkU0YUVazai3ftM+4VIp8F9e4jBChoWoJqpyvX2NPCj566BgS1BHC6stYgOE0b9g+gf3ctUudQgdCjbJ1nYQSzJkimDMvoO4gqtzM3Boi3EM5qzd/9h6+fMnEEpELDwU9Xd1Ja/Gmh1sgwowoR27sfrk5MZs0bdXy3ftzvF8Os3b36J84vW4BjnZFvrHhxZxFihbAmbn6FqlcsqF2xv3LLDoddl9KPV2rtv9XxOVa2RK92pjGvNqyTrFV0Y4bMuqojes8yvtgSV+cPXrDuRNiuSiTIm9Duj+vTNezpwRd0zgjHhb6HHJv9PMPqr/zBRdYUIGDusnzjBIQvSXZYQU5616d8mf3QWXl1PrFzLeEGZ0T15+jzR1hut35F1iv0ZN6K/SFq5fvNf0bDA1NLdcyvVEj1dFu1h68dNq4lJQBFS0ckLfLCz/6qYLrz3qoi8AnlzWX23jp84Y3WM9ypSKkx51tbpKcUUaSBozW3PFx/chRxpdxDCQaHjLoy6vwExm+VMg5mAwsuiSYZLzxXMS/MYO/qbg7QhVTEEMwtlSxVTPgb+dVUs6Cafnbo6ZjL6Fwnee249aEQlt1sF+xGllnQum4OyJonllrfhxK1HdN9i21LFjUpNClsn1NirWGvZQZoXtf7MbTx5k74GAd3hkcIa0284oT16/B/1GThSXIesz26dWouKsivNG1SvBTGBaTrE8KG7GUQCTnRGt0jIKEHZEAeVxQo1GlGfQSNFRixENSqOvXt2omKFfxPiEN0J5ZSk0ffPEbSmqp0hYkTtKCREFDoq6lXVYFfRek68z7ZAF0ZVBV1lB1HaQBTNTg4d+UcpBFQ+a3zuLMFsib3P3kcX481cJaDXHRh1f1WfexDJxndFL+HD+21GZCTChrHfVdRRIkZSf4c/fnT8vcdvIlrUq6hVraLy+qoVy+qqfjPus4P0X61e0N2ven5qVDC9w89VI3cqGlhTvQjcnV0Y86aM5+f/7z57LRYb2kPV3tyycyNYd0JtZWJ+EiIgugQuXr5GVL0qlClBndo2p0uXr9LajT5uTbIwCXkvevLfU6pSpylNGTuEChfI66cSbCSwvVjMiWOACxo/jJ8yW3jFRe53cC9RHYGobtygFrVq1lA8rveAkaKVsKengJgD+4G79idiRLVIR1VHVSXS8kzqbTNsD3w2LTshOiqsIYB37T1IlcqV9HO9yvdcIL91o4r9Zv5q8+OBxcW5LfK9LX3WmCFRdXrcYscGYquKiUzn6vVNGexBCaPu71sNf7E7B48R/WEg6i5wLkWXU3fMjGkNRp4915fBD1+0qpkQFh5jMTNmhMypVqmM1X0fPnridIdQxnFm7j5HrYpnoUwWqRmYeZ/ZvBSliB2V+q0+ZOVRlsAy0qVcDupfPb9Lix61hLWqqmzZUMbRxZqv3n+y6f/++PkrbT1j7IXKRsDf1ab0VHfu3td35fjY4f3FtJZ/VK7xWuiyhYzrkeOm+opU2cUxMCu8smW7sMN4eYl9R/UCbYGLlKkhIgoBboOohqAqWawQDezdXVy/ZMVamjlvcZAS1ZIYMWy3BHaURAnU0WKIA1Px6PETXRFlzpIsCVZYq6ekH2psgzk+ili7FMmS+GnVjr8tG01A7GlFB2r5rM09ucWLFLS6D55z7wFrsW6JpTjwr2NrFIy6v/ceqBMx4sdXR006Q/SoUcnIJNCI1dSL1nsJkau3G6uqSyx+qywH0JkypKWUKZIpO0EG5f4NRuHL129Ub+Im0ZjFEojrXpVz0/lhjahbhVyi+yGEacQwoUSDlk5ls9Ppwd40uFYBP6JalTbiDLBvvP9k24Jkz18tgb1jz0XbIhyWkjcfAtdS6Qn4u7CWQhYLtxo2ay9ELyqUc6eNEScL/7KF4IJc0Eq1Ggt7BU5YuIhGKl9/Clz/bepiEvNSBEsxjW049s8p6vrnACpUuhoNHzNZ+NClvxrJKRDV6Mg1ZdwQcT3i0jA4CYqxgiBDujRueR78CKm4eUt9wkBkoQosDnQnaMCiAj+u+E7YA23DVRV386pXAUVbZTTe0fJ+qqajYaFCAxqJqssbFtM6Uv27pxFxFj9+3EBf6OcfGHV/b9xU/7BmzpDOba+RTqPRklFIkti5THNLsmp8j7HuRQ8fPnykdRvVLdarVirrUMtztoEEHIiq8568WbPRXYrYUWhI7QJ0dmhDejmjLb2a2VaI7ZH1Clt1QIRQ773ioFu2C6LannB2xF9tyw5iDttAHCNA/BEiZi+4l6hYezdvL6bfY8aILhq7oCMehKejK6odX9D43cyz3JCatf4f7dxzQEy743opcN0toHHB33hu2YJdDhzg/Z4wdTaVqVKfyldvSHMXLheiylzwS284pugXzBgn7AMHjxwn72bthaCWCzWDGlqCWC9ZNH74tNr9Xrx0VXk9LBBa+b/OoNXq2lYOtGUXxuMnrbs3Fi2U36a/2jy/2hI0ilFVzaR3GwPgggpria2mMOZcunxN+VnF90E1CPB0jLq/WvFu6NzqaCttWyCD2dlow4Aic0b3DCKyarQu1zqP2GK5RqY1zhWy2Q4+O1UqWttAzp6/pPm+Mv7D8iNXqPHULZqWD0dpO3cnHb/hvtzx9YrYPcn1Ry/o9lPHc/lVCxjtJZEw1gSY8Vj6rSF0GzRpK0bsEC8rFkynPDmz+WZSu/s1ZbTfuk0+VKdRKypQsopoC7505TpRJYYodkWoWgpoXKT1BKH9qO4NGjGeSleuR0XL1qT+Q8YIbyseJ8W9rETKNJOypYrSkrmTxQ/f6bMXyLtpO+GJDcqt20uVsK6M6gXHC756FQcPH1Nef+7CZU1bTY2q5ckdoGV5lQqllbfhs+AoPtut00EgpuX3Jr8iJUTlr1a1OVd5twv9lld4U83B50+raY0lEO1albzSJYpQUMOo+wsb1L376pmZyhqfSz2ULFqIjE4hjfOC3qq3KpkDHDvhN87SEdCoScuiJj8vRQrlFzGrlixfvUH36zGuM2/fBSo8YJkusSpBtbv9vF00ZYd1gcQVNtgQu1ptzLW4cO+pVdKIeQoJFkIy9gnQFX3S9oETClqRYzo5dqwYtHTeFOErk01R3Jlzbd4SHM+LjnpoC96uy1/Uq88QcZ0zkXVSjKPiDPGLxWXzF6+gAUPHUqMWHalgySqUr2hF4fUeO3GGaGmNbcH+m7opfvfjt5bHp0WTBjRz0kjhc0VHrVoN/xAVftN2Bk1RDbJkTE/p06Z26TnQFhgpKpbg+J3QaCkOMXRSQ9x6163hls8i8mnRNU3FgcPHHX4eVeweFqDhxz550sRioGoOBo6obNlCZQdBQycsoFLZQI6dOE3PHLCuSLRapFevXN7pzoiWjBnWlw7vXk/zpo+lP7u1F8kKmLlwZ9qMp+/vPo2Zi3q1qrj8Ga9fpyoZnVzZs4jviCvUr11VOctpOoeozy+2wLl/hUYr8pJFTWsbalevZHUbCjGr1m7S/XqMezh09T6l7TKH+q06RC/efXTYSlKo/1Iat9W6kOEqiAM8eeuxSwsXzdmhYS3hpjCOE7BBr2biesfu/VS7YSuaMWmEEB3TJgynZEmT0OgJ00wb5uYFerIqbFoYaPJgr1y7iZo2qium9/Qmh+CkiOd4/vKViMfT8pzK18NoVYppc+R+oqo5sHc3alivhm/qQot23URlP6iLasn/OrQUgxJnwFR0uz9+V962fqOPzQzrTVt3Cj+7JWhohDQW2HecBY1Vev2vnfI2zE4gMcJRrl67Qbf+vWPlFy1SKB/df2A9tXjoyHG7nxvVAkaAyn+JotZtzLf42E8DMWezzy5q4l3H6np4jrt1akMdu/UmV8AgoGbViuK7C1uD+WAATZRcfX69GHV/V63drBRpsHE0aViHps9e6NT2oBNsujR+GxIZEZxDu7RvSX906OHU43Gsmzasq3n+cLZvAnzSSMqyJHeubGKhZKni1rMBu/YeEOlXTOCBxYe9Vx6kIeuPik6KxTMkplzJ4lKcKOEoUtjQ9PbjZ/r3v1ciaWPl0au05cxNYeOU7L54h4LVM8UQu4Nsvea77bngJcfFneTpvYj8o3mPVgOfwCZQMuikuIZ3uFLNxnTh0hVxfffOrWnO1NHC2+of1WvzZI5v300WkH5DRjv1PNguiHGkPXTr1Epch2lzaQXB7dK6IV7PwnIifdW4DT+WqxbN8BXV02YtoIbNO/xSolr+SKPq7AxD+/cSVVYVsP3YAlUjrUEcPpOopjsLtgsCXQUGT2gvrodtO627MGK6+Ld8+vzVEvg0VWkWbVv+LtZBOOuvNt8GzBKpqFuzMtWr5Xy1E9+3CaMGag6I0YgjoDHq/iKWTcsO8mfX9mKti15QEBkx6C/yFLAoEJGvesG6l/Ej+2u2LUecrLPc/Pe2WMiues2xw/uJgoslbAMxDlg8uGD/RWo0ZQul6zqbojWfSCEajKLITcdTpu5zqel0H9p82q+oZoI+gRbuLMX11es3hbhev3mbr7jasnaRWAkN4SrtE+7q1Khqu77ZZ6dTrcHRAAePgX0jY/o0omr9nUy52VrebenHFq/v5UXNGtWldcvnUK4cWcTUPZq//NV/uB9h/isxcvDfSguCFjhOwwf+ZRVRJYGnH8kYtoCw1FpIhB+2pfOnWOU92wOfWfwwVqtcTvM+M+csJr2o7CCZ0qdVesv32fBX2xPgKqGFxcd60w/wHZg0fa7m7UMH9KL6tauRMyJz5qQR4nunApV9NMEJaIy6vziPTJyq3i5U07HWJXWq5A5vD3y/y+ZPVfp/jQwGJpZZ7faO+5xpY5QzWgAWP/yGuAIyrVWYp/OY29ocyZBnGCbwCNSuKbIqja5yTVt1EQv7MGUP3/X4kQNo+sThYjpMepHdLbBl2/U/+w0TaSF4aj0LGeW2YLsG97U9xSgFtUwQgRCCoB7Qu5toRoKTM9JLcJLF88lkk18N/JDNmjKKOrdrofRLm4PPxsJZE8i7bnXN+wweOd6h10XmuVajliiRI9HyBVPFlK0jsWkZ0qWmVUtmKqfeJRhIYq2BXg4f+ceqqQ0+W5YNbTBVfPmK9mpxe+3NVTj7g75gySpRmVOByhwGUxNHD/KTyW0v4WHTqvlU4ocPVcWg4eMCbVBq1P2dt2i55mI5nHPXLZtj8zNrvmAW24PPuaeB7+/SuZOFXcfebCjWfKxbNpuKF7G2REn6Dx3j8jat3bBVNAJzBNjaMJPJMIxxCfR2hObRdPCyVq7dxHdqrGLZkuSzdhF17dhKiBtzge0Oi4hsu44s41Hjp5rsHTp/jGX1GRWNBnWq+6afANmmXNo5cBv8iFPHDRULNrHoDFVupIZUq9eMrly94eu5/hVFtQTHAO/5wR1r6X8d/hBWDEw7Q2hDjOCHDtXgfT6r/cTNqaZoHa0mYfp+yKgJNgV/t06t6cjujdTvzy6UP29OsS3YJohaZLLXqVGZFswcT9vWL7FZ4caC17/7jyBnMHVhPGD3flj46ih7NHzWrtpAJBANnbr1tfmZrl65HB3ZvV50TMVsFZrf4DuPzwKOLyq1sEqtWDCNfNYttinqMEuBzq6BhVH3F5+dzj36aW4XXh/fK3x+MZOGWQusEcBCaljesEgSaUXYJhkHJ0FqkZExHzTjOzuob3fau3UlNf+9vjgnYx9xPQbrSGXCmh+f9Ys14ztlwy493zMt0ONhi4PfLbaBMIzxCRYrSSbDKDgpKiFEm3jXpnatmvj6ZiF+5y5cJhboSF+qEK4/xLCzQtQk6k2V5/XL5wqxq3cho6wUofKONBBsn+ycKMEJumHdGsLnJ6ueSGQYOGwcnTpz3repTVC3fvTo3IY6tGmmFJvuzMK9ces2lShfW5nTrAWO/4yJI6h8meLkX+DzjaQYR/zPWsCHjiltW0BALViy0uHnPLhznc3UhKfPnlOGnEVd+ny2b9WEemos5HQX+O7hfUfUpT0wy/S7d22r6zt170sLl65yeVuMtr+Sv3t0pNbNG7ltGxBZee36TavoPsya4DMTkPTp2Zn+aOZtdf3AYWOpV9f2bnsd9CRAPwKtWS69YDYCg3Jb4DcwR4Eyv1TR5XE+6/eSYYxOoFeszZGiGieO6XMWUcmKdYQ4gEDFAjCcGHdsXCZOnqjgWEbWyUq2HruIyXJhqpy36dRLVA/wcD0CwrTNporPiEF/i+fCNkeLGkUI6cVzJtHm1Quobq0qQlQjAg1xgzXqtxCiWnZcDOqi2haTZ84XlTd3cP/BQ6pRv7kuUQ3wHrTu1FMZQecOYHNq1b67S6JadmG091mxlV+twl46CbKrXf18jp000+n0CUdA1fT3lp10iUz/xKj7i0jQNeu3uK0636ErznnuS3DyDyZNm+u28wuaOqEngrtENdi154DdhcwrVm/4pUQ1w3gqhhLWQC76g0h+8PCxqLyVr+ZNq9dtFidxZMGiIrF13WKRpIGFQEgRsUzfkA1bzBM6bL0m7ocqZ6sOPUzWDZ0eZ+mfLlmsEA3s011Mqe71WUWTxwwWdgU856mz50XcU5nK9Wjjlh2+Fhi9iyaDIp8/fRaDDa1EBUdBykWVOk01W5XbA/7F+k3aaebLOgt+NGs3auUWiwKq+8dPnNG8HekPWMymB3uDiS3bnLOBWIL1DLA+uXsQ+ejxf2IR9JHjJ8lIGHF/sS2tO/Z0Kc0C4PyI87O9rHQjgN+Gpq07O9zpVAskWFWu9bv4bXL39tnLptZa5MgwjLEwnLA2P9FIjzKqui3bd6fSlevT5OnzxEkNohk+VywE2rVlhZhGa9ygFqVNnVIkOcgW4+YJHfL5ZIdE84v00W7ftU+kcjizUFJW25s2rCMWASGu7N2798KbWr9JWypbpb44eaKazVVqazB1XKpSXacW9eE9RoMedLjUKyotge8dwqP9//4WItYV8HnAe/5biSouV6rNsdX9UKsZiC2wbVriD4MNrcYnzoCGSdXqNhOJQO4Ag+4SFWoZVuAZcX9xbuzQtTd1/XOA7pkdOfvSsVsfjxJ7GIxg0O1sK3AMRMpXa+hvMyK2jiU6tOpN5GEY5hdpEKNXlHz9+rMz4fmLl8Vl7OSZVKFMcapcvrTwRMOCAY8aLhAHOAHBAwfv34WLV+jKtRtCjKPijefT4uvXj7450q9fvxEezNChQ/n6nx0B94Nw/vrlq6hUoQ21eUKAqbJtiuRj1F7eGvWaU9dOrallk/p2k0HwfqPZ0OARE8Rnw51gcRKSMNBAo1H9mpo52SrwWUOzkDETZ/jmtLuTrTt2i657KpwR8LBAnTl3UblYCxnI7pz2BsiwL1K6umgb36xxPd1NRiDstm7fI76rRqtSe9L+zl24XHxOkdderVI5h1JvEDHX7a+BouOsp4HZHAy+e3ZpS429a4uUFnvgeA8ZMUG8h/4JBkr43UJxyBJetMgwnoOhFi/aw2Tp+NlFEWDBVYliBalY4d8oY7o0ygVwqEBCWD98/IQePHgkKhfIjH716jU9e/HCqitf8ODBKXSoUNSmRWNKkTypaCajp4INsYf7zpy3RLRNRwX9+w+ryq+O1uLFvoNGWeX/wuJTo1oFKvxbXpGxGzlyJCHwnj17TucvXqEjx0/Qug0+AeKpxYAoT67sVDBfbsqSOQMlSZSAYsaMTmFChxEiGgtX7967L34c8UOMAZUzlcBfFSRQwDKVLYupPXv8uLEpQoTwFMzLi968fiOOL97ns+cu0skz52nnngMuzyYEJkbcX6SRVChbkvLlzk6ZMqYTM24RI0Sgd+/f0+07d+mfk2dp7catbp15CUzixY0jBjqF8ufxPb98+/qVnj5/IRZjHj56QnRVhNgNKEYP7SsaCZmD36eMuYt79OfdWXjxIuOJeJSwlmilgaCimD5tKsqRLbO4pEyehGLHjuVr9XBPNODP/GpboCoNMTZx2hzqN3i0r03kV198okdYMwzD/Crgd+rM0R0iWtQcNDBr1KIj/YqwsGY8EUNbQbSQaSAAgtULMXXfv4sOerjs2ntQ3BY+XDiKFy8OJYwfV+Suxo8XhxIlTCAqM+gYFjpUSIoYMaJVq1p4Sl+9fi1Wuj989IRevHgpmiJYnvBsETy4SXwj1grTjX8PGPEj1s/UrZFhGIZhJIUK5FX+xrANhGE8C48U1pa2i28WmdT4D0IbcVRXr90QFxUQ5eHChrXyFcI6gulPcwGMZiB/dm1HWbNkoEgRI/p6TmUGtihlW4IBwJevVL1KeTG9OGbCdN/KhCvZ2wzDMEzQAk2DLEFRZ9vOfYGyPQzDBLFUEOcXO/6M3ZNxduZJIDJ+D+A+8MEiCs38gpbRUlTLfGwsekFUHlaFI6UkSpRIwpMXAotf8DrBvCi4l0XaSIgQQrR//PiJ/mjqTWuWzhId1cyztxmGYZhfm0gRI1DZUsWsrl+xdpPD7c4ZhjEGHl+xtoU9T7M9r7Rs2vLNbOEkhHez1v8TCyZb/F6fsmXJ6PD25M2VnTaunCciA+G9xgIlbANXrhmGYX5dalWvpExkWbR0daBsD8MwzvNLl0yl8Na6mAOBbVq8GExYTNC5DC1tS1WsSyPGThFJEI6kQCArG4v3Th7y8V39rad9OsMwDBN0QJIVEqgsOX7itNsjRBmG8X+CdMXaP5CNZuCtRvIHuiniMnzMZEqaOBFlz5qRMmdMR0mTJKLECRNQ7NgxTfaS128ofLiwFCtWTHrw8JHI1r5z78EPEa+vEQ3DMAwTNCwgsyaNFB2FLZkxd3GgbBPDMK7BwtqFxjWWoBEMLnrbYbu73THDMAxjDFBoSRA/rsi4R2oV1uJASCPHvEWTBpQwQTyrx9y+c4/Wb/QJlO1lGMY1WFgzDMMwjD+BZlLTJgzX9ZihoyZyQzGG8VDY3MswDMMw/sTd+w913R/rdVau3cTvB8N4KCysGYZhGMafQFSro7x4+Ypad+zJSVEM48GwsGYYhmEYf+Lxk6f0+csXu/dD2lSDpm3pzt37/F4wjAfDwpphGIZh/AksTn/48LHN+1y8fJXKVfWmo8dP8fvAMB6OyHmLlSQT99ZmGIZhGH8ACSDlyxSnnNmyUNy4sSl0qJAihvXUmQu0ZsMW2rB5m2+3X+Ynj/N58+FgPItFXYJxKgjDMAzD+CM79xwQF4Zhgj5sBWEYhmEYhmEYdwnrx7fOcOs/hmEYhmEYhnGGRV2EluaKNcMwDMMwDMO4AV9hzVVrhmEYhmEYhnGuWm1VsWZxzTAMwzAMwzD6RbXSCsLimmEYhmEYhmH0iWpNjzWLa4ZhGIZhGIZxXFTbXLzI4pphGIZhGIZhHBPVdlNBWFwzDMMwDMMwjH1R7VDcHotrhmEYhmEY5pdnkW1RDRxqaS7Fdawkmb7/8geVYRiGYRiG+XVYZF9Q6xLWquo1i2yGYRiGYRjmVxfTZMb/ATrOjCJMW0DrAAAAAElFTkSuQmCC';
11
+ const FIELDS = [
12
+ { k: 'input', label: '源文件', ph: '例如 /Users/mac/通知.md(.md/.txt)' },
13
+ { k: 'out', label: '输出目录', ph: '例如 /Users/mac/公文排版输出' },
14
+ { k: 'title', label: '标题', ph: '可留空,取 Markdown 第一个 # 标题' },
15
+ { k: 'sender', label: '发文机关', ph: '可留空' },
16
+ { k: 'date', label: '成文日期', ph: '可留空,例如 2026年10月29日' },
17
+ ];
18
+ const FORMATS = [['docx', 'Word'], ['pdf', 'PDF'], ['txt', 'TXT']];
19
+ const Ico = ({ size = 18 } = {}) => h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round' }, [
20
+ h('path', { key: 'a', d: 'M6 3h8l4 4v14H6z' }), h('path', { key: 'b', d: 'M14 3v4h4' }), h('path', { key: 'c', d: 'M9 12h6M9 16h6' }),
21
+ ]);
22
+ const CSS = `
23
+ .gn-root{position:relative;display:flex;flex-direction:column;gap:14px;padding:18px 22px 52px;height:100%;overflow:auto}
24
+ .gn-head{display:flex;align-items:center;gap:10px}
25
+ .gn-title{font-size:16px;font-weight:600}
26
+ .gn-sub{color:var(--dsw-alias-text-secondary,#8b98a8);font-size:12px}
27
+ .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}
28
+ .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}
29
+ .gn-row{display:flex;gap:8px;align-items:center}
30
+ .gn-label{font-size:12px;color:var(--dsw-alias-text-secondary,#8b98a8);width:92px;flex:none}
31
+ .gn-input{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}
32
+ .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}
33
+ .gn-btn[disabled]{opacity:.55;cursor:default}
34
+ .gn-chk{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--dsw-alias-text-secondary,#8b98a8);cursor:pointer}
35
+ .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}
36
+ .gn-ok{color:var(--dsw-alias-state-success-primary,#46b47a)}.gn-err{color:var(--dsw-alias-state-error-primary,#e0645a)}
37
+ .gn-tip{font-size:12px;color:var(--dsw-alias-text-secondary,#8b98a8)}
38
+ .gn-badge{position:absolute;right:16px;bottom:12px;display:inline-flex;opacity:.9}
39
+ .gn-badge img{width:121px;height:20px;display:block}
40
+ `;
41
+ function 面板() {
42
+ const [values, 设values] = react.useState(() => Object.fromEntries(FIELDS.map((f) => [f.k, ''])));
43
+ const [fmts, 设fmts] = react.useState(['docx', 'pdf', 'txt']);
44
+ const [版本, 设版本] = react.useState('');
45
+ const [跑, 设跑] = react.useState(false);
46
+ const [结果, 设结果] = react.useState(null);
47
+ react.useEffect(() => { void (async () => {
48
+ try { const r = await fetch(`${API}/info`); const j = await r.json(); if (j.version) 设版本(String(j.version)); } catch {}
49
+ })(); }, []);
50
+ const 切换 = (k) => 设fmts(fmts.includes(k) ? fmts.filter((x) => x !== k) : [...fmts, k]);
51
+ const 执行 = async () => {
52
+ 设跑(true); 设结果(null);
53
+ try {
54
+ const res = await fetch(`${API}/run`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ...values, formats: fmts }) });
55
+ 设结果(await res.json());
56
+ } catch (e) {
57
+ 设结果({ ok: false, text: '面板后端未连接:' + String(e) + '(插件宿主未注册出口时,改用 skill 脚本)' });
58
+ } finally { 设跑(false); }
59
+ };
60
+ return h('div', { className: 'gn-root' }, [
61
+ h('style', { key: 'c' }, CSS),
62
+ h('div', { key: 'h', className: 'gn-head' }, [
63
+ h(Ico, { key: 'i' }),
64
+ h('div', { key: 't' }, [
65
+ h('div', { key: 'a', className: 'gn-title' }, '深鲸公文排版'),
66
+ h('div', { key: 'b', className: 'gn-sub' }, ['GB/T 9704-2012 · Markdown → docx / pdf / txt', 版本 ? h('span', { key: 'v', className: 'gn-ver' }, 'v' + 版本) : null]),
67
+ ]),
68
+ ]),
69
+ h('div', { key: 'f', className: 'gn-card' }, [
70
+ h('div', { key: 'fmt', className: 'gn-row' }, [
71
+ h('span', { key: 'l', className: 'gn-label' }, '输出格式'),
72
+ ...FORMATS.map(([k, label]) => h('label', { key: k, className: 'gn-chk' }, [
73
+ h('input', { key: 'i', type: 'checkbox', checked: fmts.includes(k), onChange: () => 切换(k) }),
74
+ label,
75
+ ])),
76
+ ]),
77
+ ...FIELDS.map((f) => h('div', { key: f.k, className: 'gn-row' }, [
78
+ h('span', { key: 'l', className: 'gn-label' }, f.label),
79
+ h('input', { key: 'i', className: 'gn-input', placeholder: f.ph || '', value: values[f.k], onChange: (e) => 设values({ ...values, [f.k]: e.target.value }) }),
80
+ ])),
81
+ h('div', { key: 'a', className: 'gn-row', style: { justifyContent: 'flex-end' } }, [
82
+ h('button', { key: 'b', className: 'gn-btn', disabled: 跑 || !fmts.length, onClick: 执行 }, 跑 ? '排版中…' : '开始排版'),
83
+ ]),
84
+ ]),
85
+ 结果 ? h('div', { key: 'r', className: 'gn-card' }, [
86
+ h('div', { key: 's', className: 结果.ok === false ? 'gn-err' : 'gn-ok' },
87
+ 结果.ok === false ? ('失败:' + (结果.text || '未知错误')) : '排版完成'),
88
+ 结果.ok === false ? null : h('div', { key: 'files' }, (结果.outputs && 结果.outputs.files ? 结果.outputs.files.map((f) =>
89
+ h('div', { key: f.name, className: 'gn-tip' }, '✅ ' + f.name + ' ' + (f.bytes / 1024).toFixed(1) + ' KB')) : null)),
90
+ 结果.ok === false ? null : h('div', { key: 'o', className: 'gn-out' }, 结果.text || ''),
91
+ ]) : null,
92
+ h('div', { key: 'tip', className: 'gn-tip' }, '字体需本机自备(DSH_CN_FANGSONG 或系统字体目录)。PDF/Word 排版脚本随插件分发,需 Python 3。'),
93
+ h('a', { key: 'bg', className: 'gn-badge', href: 'https://deepwhale.org.cn', target: '_blank', rel: 'noopener noreferrer', title: '深鲸桌面 · deepwhale.org.cn' },
94
+ h('img', { src: BADGE, width: 121, height: 20, alt: 'powered by dwh' })),
95
+ ]);
96
+ }
97
+ const inject = ['slots'];
98
+ function apply(ctx) {
99
+ const 试 = (n, f) => { try { f(); } catch (e) { try { console.warn('[cn-doc-formatter] 插槽注册失败:' + n, e && e.message); } catch {} } };
100
+ 试('侧栏图标', () => ctx.slots.inject(SLOT_ICON, function* () { yield ctx.slots.register({ name: SLOT_ICON, id: PANEL_ID, order: 49, label: '深鲸公文排版' }, Ico); }));
101
+ 试('主面板', () => ctx.slots.inject(SLOT_MAIN, function* () { yield ctx.slots.register({ name: SLOT_MAIN, key: PANEL_ID }, 面板); }));
102
+ }
103
+ exports.inject = inject; exports.apply = apply; return module.exports;
104
+ },
105
+ });
package/lib/index.js CHANGED
@@ -28,6 +28,7 @@
28
28
  import { readFileSync } from 'node:fs';
29
29
  import { fileURLToPath } from 'node:url';
30
30
  import { dirname, join } from 'node:path';
31
+ import { registerApi } from './api.js';
31
32
 
32
33
  /** 本包根目录(lib/ 的上一级) */
33
34
  const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
@@ -38,7 +39,7 @@ const skillDir = join(packageRoot, 'skill');
38
39
  const SKILL_DIR_PLACEHOLDER = '<skill目录>';
39
40
 
40
41
  export const name = 'cn-doc-formatter';
41
- export const inject = ['skills'];
42
+ export const inject = ['skills', 'webServer'];
42
43
 
43
44
  /** 技能标识:kebab-case。刻意避开 'chinese-doc-formatter',见文件头说明 */
44
45
  const SKILL_NAME = 'cn-doc-formatter';
@@ -54,7 +55,7 @@ function skillBody() {
54
55
  return raw.split(SKILL_DIR_PLACEHOLDER).join(skillDir);
55
56
  }
56
57
 
57
- export function apply(ctx) {
58
+ function applySkill(ctx) {
58
59
  // skills 服务缺失时静默跳过:这是一个可选能力,不应阻断 profile 启动
59
60
  if (!ctx.skills || typeof ctx.skills.register !== 'function') {
60
61
  ctx.logger?.warn?.('[cn-doc-formatter] 未找到 skills 服务,跳过技能注册');
@@ -86,3 +87,13 @@ export function apply(ctx) {
86
87
 
87
88
  ctx.logger?.info?.(`[cn-doc-formatter] 已注册技能 ${SKILL_NAME}`);
88
89
  }
90
+
91
+ /** 面板出口:技能注册照旧,额外把「选 md → 出 docx/pdf」挂到宿主 webServer */
92
+ export function apply(ctx) {
93
+ applySkill(ctx);
94
+ try {
95
+ if (ctx.webServer?.register) registerApi(ctx);
96
+ } catch (e) {
97
+ ctx.logger?.warn?.('[cn-doc-formatter] 面板出口注册失败: ' + (e?.message || e));
98
+ }
99
+ }
package/lib/run.mjs ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * 面板后端:选 .md/.txt + 输出目录 → 调随包 python 脚本出 docx / pdf / txt。
3
+ *
4
+ * 复用 skill/scripts 里已有的排版脚本,不重写排版逻辑:
5
+ * md_adapter.py Markdown → 公文体正文
6
+ * docx_formatter.py / pdf_formatter.py / txt_formatter.py → 三种产物
7
+ *
8
+ * Python 或依赖缺失时返回可读错误,不抛异常(面板不能白屏)。
9
+ */
10
+ import { execFile } from 'node:child_process';
11
+ import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync } from 'node:fs';
12
+ import { basename, extname, join, resolve } from 'node:path';
13
+ import { tmpdir } from 'node:os';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const PYTHON = process.env.DSH_PYTHON || 'python3';
17
+ const MAX_BODY = 100000;
18
+
19
+ function script(name) {
20
+ return join(fileURLToPath(new URL('.', import.meta.url)), '..', 'skill', 'scripts', name);
21
+ }
22
+
23
+ function runPy(args, timeout = 120000) {
24
+ return new Promise((resolveP) => {
25
+ execFile(PYTHON, args, { timeout, maxBuffer: 8 * 1024 * 1024, env: process.env }, (err, stdout, stderr) => {
26
+ if (err) {
27
+ if (err.code === 'ENOENT') {
28
+ resolveP({ ok: false, text: `找不到 ${PYTHON}。本插件的排版脚本需要 Python 3 + python-docx + reportlab。` });
29
+ return;
30
+ }
31
+ if (err.killed) { resolveP({ ok: false, text: '排版脚本执行超时(120 秒)' }); return; }
32
+ const tail = String(stderr || '').trim().split('\n').slice(-6).join('\n');
33
+ resolveP({ ok: false, text: `排版脚本失败:${tail || err.message}` });
34
+ return;
35
+ }
36
+ resolveP({ ok: true, text: String(stdout || '').trim() });
37
+ });
38
+ });
39
+ }
40
+
41
+ function firstHeading(md) {
42
+ for (const line of String(md).split(/\r?\n/)) {
43
+ const m = line.match(/^#\s+(.+?)\s*$/);
44
+ if (m) return m[1].replace(/[*_`]/g, '').trim();
45
+ }
46
+ return '';
47
+ }
48
+
49
+ function safeName(s) {
50
+ return String(s).replace(/[\\/:*?"<>|\n\r\t]/g, '_').replace(/\s+/g, ' ').trim().slice(0, 80);
51
+ }
52
+
53
+ export async function runPanel(opts = {}) {
54
+ const input = String(opts.input || '').trim();
55
+ const out = String(opts.out || '').trim();
56
+ const titleOpt = String(opts.title || '').trim();
57
+ const sender = String(opts.sender || '').trim();
58
+ const date = String(opts.date || '').trim();
59
+ const formats = Array.isArray(opts.formats) && opts.formats.length
60
+ ? opts.formats.filter((f) => ['docx', 'pdf', 'txt'].includes(f))
61
+ : ['docx', 'pdf', 'txt'];
62
+
63
+ if (!input) return { ok: false, text: '请填写要排版的 .md / .txt 文件路径' };
64
+ if (!out) return { ok: false, text: '请填写输出目录' };
65
+ if (!existsSync(input)) return { ok: false, text: '文件不存在:' + resolve(input) };
66
+ const inputPath = resolve(input);
67
+ if (!statSync(inputPath).isFile()) return { ok: false, text: '请输入单个文件(暂不支持整个目录)' };
68
+ const ext = extname(inputPath).toLowerCase();
69
+ if (!['.md', '.markdown', '.txt'].includes(ext)) return { ok: false, text: '只支持 .md / .txt 输入' };
70
+ if (!formats.length) return { ok: false, text: '至少要选一种输出格式(docx / pdf / txt)' };
71
+
72
+ let raw;
73
+ try { raw = readFileSync(inputPath, 'utf8'); }
74
+ catch (e) { return { ok: false, text: `读取失败:${e?.message || e}` }; }
75
+
76
+ const title = titleOpt || (ext === '.txt' ? basename(inputPath, ext) : firstHeading(raw)) || basename(inputPath, ext);
77
+ const base = safeName(title) || safeName(basename(inputPath, ext)) || '公文';
78
+
79
+ let body = raw;
80
+ let tmpBody = '';
81
+ try {
82
+ if (ext !== '.txt') {
83
+ tmpBody = join(tmpdir(), `dsh-cndoc-${process.pid}-${Date.now()}.txt`);
84
+ const conv = await runPy([script('md_adapter.py'), '--input', inputPath, '--output', tmpBody]);
85
+ if (!conv.ok) return { ok: false, text: 'Markdown 转公文体失败:' + conv.text };
86
+ body = readFileSync(tmpBody, 'utf8');
87
+ }
88
+ if (!body.trim()) return { ok: false, text: '正文为空:Markdown 里除了标题没有可排版的内容' };
89
+ if (body.length > MAX_BODY) return { ok: false, text: `正文过长(${body.length} 字符,上限 ${MAX_BODY})` };
90
+
91
+ const outDir = resolve(out);
92
+ try { mkdirSync(outDir, { recursive: true }); }
93
+ catch (e) { return { ok: false, text: `无法创建输出目录:${outDir}(${e?.message || e})` }; }
94
+
95
+ const written = [];
96
+ for (const fmt of formats) {
97
+ const filename = `${base}.${fmt}`;
98
+ const args = [
99
+ script(`${fmt}_formatter.py`),
100
+ '--output', outDir,
101
+ '--filename', filename,
102
+ '--title', title,
103
+ '--body', body,
104
+ ];
105
+ if (sender) args.push('--sender', sender);
106
+ if (date) args.push('--date', date);
107
+ const r = await runPy(args);
108
+ if (!r.ok) return { ok: false, text: `${fmt} 生成失败:${r.text}` };
109
+ const full = join(outDir, filename);
110
+ if (!existsSync(full)) return { ok: false, text: `${fmt} 脚本未产出文件:${full}` };
111
+ written.push({ name: filename, path: full, bytes: statSync(full).size });
112
+ }
113
+
114
+ const lines = [
115
+ `已生成 ${written.length} 份文件 → ${outDir}`,
116
+ ` 源文件 ${inputPath}`,
117
+ ` 标题 ${title}`,
118
+ '',
119
+ ...written.map((w) => ` ✅ ${w.name} ${(w.bytes / 1024).toFixed(1)} KB ${w.path}`),
120
+ '',
121
+ '提示:字体由本机自备(DSH_CN_FANGSONG 或系统字体目录);PDF 缺仿宋时会退回宋体并在 stderr 给出指引。',
122
+ '如需逐项核对页边距/字号/行距,可再跑 skill/scripts/verify_output.py --dir <输出目录>。',
123
+ ];
124
+ return { ok: true, text: lines.join('\n'), outputs: { dir: outDir, files: written }, title };
125
+ } finally {
126
+ if (tmpBody) { try { unlinkSync(tmpBody); } catch {} }
127
+ }
128
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepwhale-cn/dsh-cn-doc-formatter",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "中文公文排版:按 GB/T 9704-2012 生成 .docx / .pdf / .txt,附输出自检。不含字体文件,字体由用户自备。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,14 +33,23 @@
33
33
  "main": "./lib/index.js",
34
34
  "exports": {
35
35
  ".": "./lib/index.js",
36
+ "./api": "./lib/api.js",
37
+ "./client": "./lib/client.js",
36
38
  "./package.json": "./package.json"
37
39
  },
38
40
  "engines": {
39
- "dsh": ">=0.1.0-rc.6"
41
+ "dsh": ">=0.1.0-rc.6 <0.3.0"
40
42
  },
41
43
  "dsh": {
42
44
  "bundle": {
43
45
  "patch": "./cordis.patch.yml"
46
+ },
47
+ "client": {
48
+ "inject": [
49
+ "@deepseek-ai/dsh-client-runtime"
50
+ ],
51
+ "platform": "web",
52
+ "immediately": true
44
53
  }
45
54
  },
46
55
  "peerDependencies": {
@@ -443,8 +443,11 @@ def generate_docx(args):
443
443
  add_title(doc, args.title)
444
444
 
445
445
  if args.body:
446
- for line in args.body.strip().split('\n'):
447
- line = line.strip()
446
+ for kind, payload in split_table_blocks(args.body):
447
+ if kind == 'table':
448
+ add_table(doc, payload)
449
+ continue
450
+ line = payload.strip()
448
451
  if not line:
449
452
  continue
450
453
  if is_level1_title(line):
@@ -476,6 +479,91 @@ def generate_docx(args):
476
479
  return filepath
477
480
 
478
481
 
482
+ def _table_sep(line):
483
+ """markdown 表格的分隔行(|---|---|)。"""
484
+ import re as _re
485
+ return bool(_re.match(r'^\s*\|[\s:|-]+\|\s*$', line))
486
+
487
+
488
+ def split_table_blocks(body):
489
+ """把正文切成 [('line', 文本) | ('table', [[单元格,…], …])]。
490
+
491
+ 约定:**连续以 | 开头**的行视为一个 markdown 表格(含可选分隔行)。
492
+ 这样 md_adapter 原样透传 md 表格时,下游就能渲染成真表格;
493
+ 不在表格里的 | 行也照样按表格处理(旧行为是拍平,新行为更好)。
494
+ """
495
+ blocks, buf = [], []
496
+
497
+ def flush():
498
+ if not buf:
499
+ return
500
+ rows = []
501
+ for r in buf:
502
+ if _table_sep(r):
503
+ continue
504
+ cells = [c.strip() for c in r.strip().strip('|').split('|')]
505
+ if any(cells):
506
+ rows.append(cells)
507
+ if rows:
508
+ blocks.append(('table', rows))
509
+
510
+ for raw in body.split('\n'):
511
+ if raw.strip().startswith('|'):
512
+ buf.append(raw.rstrip())
513
+ continue
514
+ flush()
515
+ buf = []
516
+ blocks.append(('line', raw))
517
+ flush()
518
+ return blocks
519
+
520
+
521
+ def add_table(doc, rows):
522
+ """把二维数组渲染成**真 Word 表格**(带边框、仿宋、五号 10.5pt、表头加粗)。
523
+
524
+ 与全文风格一致:字体用同一个仿宋族(CN_FONT),行距用固定值保持紧凑。
525
+ 单元格里换行会出现孤立标点的问题由上游 md_adapter 负责合并。
526
+ """
527
+ from docx.enum.table import WD_TABLE_ALIGNMENT
528
+ from docx.shared import Pt
529
+ from docx.oxml.ns import qn
530
+ from docx.oxml import OxmlElement
531
+ from docx.enum.text import WD_LINE_SPACING
532
+
533
+ if not rows:
534
+ return
535
+ ncol = max(len(r) for r in rows)
536
+ table = doc.add_table(rows=0, cols=ncol)
537
+ table.alignment = WD_TABLE_ALIGNMENT.CENTER
538
+ for ri, row in enumerate(rows):
539
+ cells = table.add_row().cells
540
+ for ci in range(ncol):
541
+ txt = row[ci] if ci < len(row) else ''
542
+ cell = cells[ci]
543
+ cell.text = ''
544
+ p = cell.paragraphs[0]
545
+ p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY
546
+ p.paragraph_format.line_spacing = Pt(16)
547
+ run = p.add_run(txt)
548
+ run.font.name = CN_FONT
549
+ run.font.size = Pt(10.5)
550
+ run.bold = (ri == 0)
551
+ try:
552
+ run._element.rPr.rFonts.set(qn('w:eastAsia'), CN_FONT)
553
+ except Exception:
554
+ pass
555
+ tcPr = cell._tc.get_or_add_tcPr()
556
+ borders = OxmlElement('w:tcBorders')
557
+ for edge in ('top', 'left', 'bottom', 'right'):
558
+ el = OxmlElement('w:' + edge)
559
+ el.set(qn('w:val'), 'single')
560
+ el.set(qn('w:sz'), '6')
561
+ el.set(qn('w:color'), '808080')
562
+ borders.append(el)
563
+ tcPr.append(borders)
564
+ add_body_text(doc, '')
565
+
566
+
479
567
  def main():
480
568
  parser = argparse.ArgumentParser(description='党政机关公文排版格式生成器 (.docx)')
481
569
  parser.add_argument('--output', required=True, help='输出目录')
@@ -101,37 +101,12 @@ def md_to_body(md: str, drop_meta: bool = False) -> str:
101
101
  nonlocal table_rows
102
102
  if not table_rows:
103
103
  return
104
- rows = [r for r in table_rows
105
- if not re.match(r'^\s*\|[\s:|-]+\|\s*$', r)]
104
+ # ── 表格:**原样保留**(markdown 管道形式)──────────────────────
105
+ # 原来这里把表格拍平成「表头:值;表头:值。」—— 那是本 skill 早期没有
106
+ # 表格支持时的权宜做法。2026-10-02 起 docx / pdf / txt 三个 formatter
107
+ # 都会把连续 | 行渲染成**真表格**,所以这里透传即可(拍平反而丢信息)。
108
+ out.extend(table_rows)
106
109
  table_rows = []
107
- if not rows:
108
- return
109
- cells = [[c.strip() for c in r.strip().strip('|').split('|')] for r in rows]
110
- head = cells[0]
111
- generic = {'#', '序号', '编号'}
112
- for row in cells[1:]:
113
- if not any(row):
114
- continue
115
- # 两列表:左列即键(如「项 / 内容」表里的 运营主体、官网 …)
116
- if len(row) == 2:
117
- k, v = clean_inline(row[0]), clean_inline(row[1])
118
- if k and v:
119
- out.append(f'  {k}:{v}。')
120
- elif v:
121
- out.append('  ' + v + '。')
122
- continue
123
- # 多列表:表头属于 generic 的整列跳过,其余用「表头:值」
124
- parts = []
125
- for i, c in enumerate(row):
126
- if not c:
127
- continue
128
- key = (head[i] if i < len(head) else '').strip()
129
- if key in generic:
130
- continue
131
- val = clean_inline(c)
132
- parts.append(f'{clean_inline(key)}:{val}' if key else val)
133
- if parts:
134
- out.append('  ' + ';'.join(parts) + '。')
135
110
 
136
111
  for raw in lines:
137
112
  line = raw.rstrip()
@@ -330,8 +330,12 @@ def generate_pdf(args):
330
330
 
331
331
  # === 正文 ===
332
332
  if args.body:
333
- for line in args.body.strip().split('\n'):
334
- line = line.strip()
333
+ for kind, payload in split_table_blocks(args.body):
334
+ if kind == 'table':
335
+ story.append(make_table(payload, make_style, body_size, markup_text))
336
+ story.append(Spacer(1, body_leading * 0.5))
337
+ continue
338
+ line = payload.strip()
335
339
  if not line:
336
340
  continue
337
341
  if is_level1_title(line):
@@ -381,6 +385,83 @@ def generate_pdf(args):
381
385
  return filepath
382
386
 
383
387
 
388
+ def _table_sep(line):
389
+ """markdown 表格的分隔行(|---|---|)。"""
390
+ import re as _re
391
+ return bool(_re.match(r'^\s*\|[\s:|-]+\|\s*$', line))
392
+
393
+
394
+ def split_table_blocks(body):
395
+ """把正文切成 [('line', 文本) | ('table', [[单元格,…], …])]。
396
+
397
+ 约定:**连续以 | 开头**的行视为一个 markdown 表格(含可选分隔行)。
398
+ 这样 md_adapter 原样透传 md 表格时,下游就能渲染成真表格;
399
+ 不在表格里的 | 行也照样按表格处理(旧行为是拍平,新行为更好)。
400
+ """
401
+ blocks, buf = [], []
402
+
403
+ def flush():
404
+ if not buf:
405
+ return
406
+ rows = []
407
+ for r in buf:
408
+ if _table_sep(r):
409
+ continue
410
+ cells = [c.strip() for c in r.strip().strip('|').split('|')]
411
+ if any(cells):
412
+ rows.append(cells)
413
+ if rows:
414
+ blocks.append(('table', rows))
415
+
416
+ for raw in body.split('\n'):
417
+ if raw.strip().startswith('|'):
418
+ buf.append(raw.rstrip())
419
+ continue
420
+ flush()
421
+ buf = []
422
+ blocks.append(('line', raw))
423
+ flush()
424
+ return blocks
425
+
426
+
427
+ def make_table(rows, make_style, body_size, markup_text):
428
+ """把二维数组渲染成**真 PDF 表格**(可跨页、带边框、表头加粗效果)。"""
429
+ from reportlab.platypus import Table, TableStyle
430
+ from reportlab.lib import colors
431
+ from reportlab.lib.units import cm
432
+
433
+ if not rows:
434
+ return Spacer(1, 1)
435
+ ncol = max(len(r) for r in rows)
436
+ # 列宽按内容长度加权(越长的列越宽),避免窄列挤成一团
437
+ weights = []
438
+ for ci in range(ncol):
439
+ w = max((len(r[ci]) for r in rows if ci < len(r)), default=4)
440
+ weights.append(max(w, 5))
441
+ total = float(sum(weights))
442
+ avail = A4[0] - 2.8 * cm - 2.6 * cm
443
+ widths = [avail * w / total for w in weights]
444
+
445
+ cell_style = make_style(FONTS['body'], 9.5, TA_JUSTIFY, leading=13)
446
+ data = []
447
+ for ri, row in enumerate(rows):
448
+ cells = []
449
+ for ci in range(ncol):
450
+ txt = row[ci] if ci < len(row) else ''
451
+ cells.append(Paragraph(markup_text(txt), cell_style))
452
+ data.append(cells)
453
+ t = Table(data, colWidths=widths, repeatRows=1)
454
+ t.setStyle(TableStyle([
455
+ ('GRID', (0, 0), (-1, -1), 0.4, colors.HexColor('#9aa3ad')),
456
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
457
+ ('LEFTPADDING', (0, 0), (-1, -1), 4),
458
+ ('RIGHTPADDING', (0, 0), (-1, -1), 4),
459
+ ('TOPPADDING', (0, 0), (-1, -1), 3),
460
+ ('BOTTOMPADDING', (0, 0), (-1, -1), 3),
461
+ ]))
462
+ return t
463
+
464
+
384
465
  def main():
385
466
  parser = argparse.ArgumentParser(description='党政机关公文排版格式生成器 (.pdf)')
386
467
  parser.add_argument('--output', required=True, help='输出目录')
@@ -37,9 +37,28 @@ def generate_txt(args):
37
37
 
38
38
  # 正文
39
39
  if args.body:
40
- body_lines = args.body.strip().split('\n')
41
- for line in body_lines:
42
- line = line.strip()
40
+ for kind, payload in split_table_blocks(args.body):
41
+ if kind == 'table':
42
+ # 纯文本表格:按列宽对齐(中文字符按 2 宽计算)
43
+ ncol = max(len(r) for r in payload)
44
+ widths = [0] * ncol
45
+ for row in payload:
46
+ for ci, c in enumerate(row):
47
+ w = sum(2 if ord(ch) > 127 else 1 for ch in c)
48
+ widths[ci] = max(widths[ci], w)
49
+
50
+ def pad(s, w):
51
+ n = sum(2 if ord(ch) > 127 else 1 for ch in s)
52
+ return s + ' ' * max(0, w - n)
53
+
54
+ for ri, row in enumerate(payload):
55
+ cells = [pad(row[ci] if ci < len(row) else '', widths[ci]) for ci in range(ncol)]
56
+ lines.append(' ' + ' '.join(cells).rstrip())
57
+ if ri == 0:
58
+ lines.append(' ' + '-' * (sum(widths) + 2 * (ncol - 1)))
59
+ lines.append('')
60
+ continue
61
+ line = payload.strip()
43
62
  if not line:
44
63
  continue
45
64
  # 检测标题级别(与 docx/pdf 脚本保持一致的严格规则)
@@ -88,6 +107,45 @@ def is_heading(line):
88
107
  return False
89
108
 
90
109
 
110
+ def _table_sep(line):
111
+ """markdown 表格的分隔行(|---|---|)。"""
112
+ import re as _re
113
+ return bool(_re.match(r'^\s*\|[\s:|-]+\|\s*$', line))
114
+
115
+
116
+ def split_table_blocks(body):
117
+ """把正文切成 [('line', 文本) | ('table', [[单元格,…], …])]。
118
+
119
+ 约定:**连续以 | 开头**的行视为一个 markdown 表格(含可选分隔行)。
120
+ 这样 md_adapter 原样透传 md 表格时,下游就能渲染成真表格;
121
+ 不在表格里的 | 行也照样按表格处理(旧行为是拍平,新行为更好)。
122
+ """
123
+ blocks, buf = [], []
124
+
125
+ def flush():
126
+ if not buf:
127
+ return
128
+ rows = []
129
+ for r in buf:
130
+ if _table_sep(r):
131
+ continue
132
+ cells = [c.strip() for c in r.strip().strip('|').split('|')]
133
+ if any(cells):
134
+ rows.append(cells)
135
+ if rows:
136
+ blocks.append(('table', rows))
137
+
138
+ for raw in body.split('\n'):
139
+ if raw.strip().startswith('|'):
140
+ buf.append(raw.rstrip())
141
+ continue
142
+ flush()
143
+ buf = []
144
+ blocks.append(('line', raw))
145
+ flush()
146
+ return blocks
147
+
148
+
91
149
  def main():
92
150
  parser = argparse.ArgumentParser(description='党政机关公文排版格式生成器 (.txt)')
93
151
  parser.add_argument('--output', required=True, help='输出目录')