feat(runtime): add document tools, MiSans and patched chat manager
This commit is contained in:
@@ -1,5 +1,21 @@
|
||||
# DSH Runtime Release
|
||||
|
||||
## 内置修复版 Chat Manager
|
||||
|
||||
镜像内置 `[email protected]`(基于官方 1.5.12,保留 MIT 许可及服务端安全校验)。补丁仅针对客户端:删除、恢复、归档搜索在面板内使用当前 `/dsh/<实例ID>/` 前缀,独立 DSH 根路径部署保持原行为;非 JSON 响应显示 HTTP 状态和代理排查提示,不再直接暴露 JSON 解析异常。不会修改全局 fetch、绕过 Cookie/Origin/可信主机校验或自动重试删除请求。
|
||||
|
||||
新建空 web profile 默认启用此插件,内置包和 clsx 依赖均支持离线安装。已有 profile 不会被覆盖,也不会自动替换用户已安装的官方版本。升级镜像后,如需迁移已有实例,请保留其他设置,将 profile 中同名依赖改为 `file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.12-dsh-mgr.1.tgz`,并设置 pnpm override `dsh-chat-manager>clsx` 为 `file:/usr/local/lib/dsh-mgr/clsx-2.1.1.tgz`,确保 bundles 中只有一次 `dsh-chat-manager`,再重启实例。不要同时启用重复的手动 patch 插件条目。
|
||||
|
||||
域名访问仍要求 DSH 配置相应 trusted host,且 runtime 必须经过面板认证与实例归属校验。此修复不开放面板根路径的插件接口。
|
||||
|
||||
## 内置 Python 与 LibreOffice
|
||||
|
||||
Runtime 镜像包含 Debian Bookworm 的 Python 3、pip、venv(`python` 指向 `python3`),以及 LibreOffice 和 Noto CJK 中文字体。LibreOffice 可通过 `libreoffice --headless` 无界面运行。Python 项目依赖请安装到工作区的虚拟环境中,不要覆盖系统 Python 包。
|
||||
|
||||
这些依赖在镜像构建时安装;已有实例需要重建镜像并升级后才生效。
|
||||
|
||||
另包含小米 MiSans 简体中文字体的 10 个静态 TTF 字重,保留 Noto CJK,不修改默认字体优先级。构建时从小米官方下载并校验固定 SHA-256,仅安装原始 TTF 文件;上游包变化会令构建失败,需审核后更新校验值。字体及声明位于 `/usr/local/share/fonts/misans`。MiSans 使用小米专有免费商用许可,并非开源字体许可;安装使用及对外分发镜像前应阅读[官方许可协议](https://hyperos.mi.com/font/zh/download/),不得单独分发或售卖字体。
|
||||
|
||||
## runtime-7.3:域名访问设置适配
|
||||
|
||||
本次待发布修订为 runtime-7.3。构建时对 DSH 0.2.0-rc.2 的设置 UI 应用精确补丁,使设置视图与表单使用 host 持久化;不修改 connection.isLoopback、可信主机、Origin 或认证逻辑。版本或目标语句不匹配时构建失败,升级上游版本必须重新审核补丁。
|
||||
|
||||
+11
-1
@@ -3,13 +3,20 @@ FROM node:24-bookworm-slim
|
||||
ARG DSH_VERSION
|
||||
# Keep the historical dependency-layer cache key; the template revision is set below.
|
||||
ARG RECIPE_REV=3
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends tini ca-certificates git procps && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tini ca-certificates git procps \
|
||||
python3 python3-pip python3-venv python-is-python3 \
|
||||
libreoffice fonts-noto-cjk \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN python --version && python3 -m pip --version && libreoffice --headless --version
|
||||
RUN test -n "$DSH_VERSION" && npm install --global --foreground-scripts --loglevel=verbose --registry=https://registry.npmjs.org "@deepseek-ai/dsh@${DSH_VERSION}"
|
||||
RUN npm install --global --ignore-scripts --registry=https://registry.npmjs.org [email protected] && test "$(pnpm --version)" = "10.11.0"
|
||||
COPY install-mobile.mjs /usr/local/lib/dsh-mgr/install-mobile.mjs
|
||||
RUN node /usr/local/lib/dsh-mgr/install-mobile.mjs
|
||||
COPY install-free-search.mjs /usr/local/lib/dsh-mgr/install-free-search.mjs
|
||||
RUN node /usr/local/lib/dsh-mgr/install-free-search.mjs
|
||||
COPY install-chat-manager.mjs patch-chat-manager.mjs /usr/local/lib/dsh-mgr/
|
||||
RUN node /usr/local/lib/dsh-mgr/install-chat-manager.mjs
|
||||
COPY install-workspace.mjs /usr/local/lib/dsh-mgr/install-workspace.mjs
|
||||
RUN node /usr/local/lib/dsh-mgr/install-workspace.mjs
|
||||
COPY init-profile.mjs /usr/local/lib/dsh-mgr/init-profile.mjs
|
||||
@@ -22,6 +29,9 @@ RUN cp /usr/bin/caddy /usr/bin/caddy.clean && mv /usr/bin/caddy.clean /usr/bin/c
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
COPY entrypoint.sh /usr/local/bin/dsh-entrypoint
|
||||
RUN chmod 755 /usr/local/bin/dsh-entrypoint && mkdir -p /data /workspace /shared && ln -s /workspace /data/dsh-home && chown -R node:node /data /workspace /shared
|
||||
COPY install-misans.mjs /usr/local/lib/dsh-mgr/install-misans.mjs
|
||||
RUN node /usr/local/lib/dsh-mgr/install-misans.mjs
|
||||
COPY MiSans-NOTICE.txt /usr/local/share/fonts/misans/NOTICE.txt
|
||||
ARG RECIPE_REV=7
|
||||
LABEL dsh-mgr.role=runtime dsh-mgr.version=$DSH_VERSION dsh-mgr.recipe=$RECIPE_REV
|
||||
ENV DSH_HOME=/workspace HOME=/workspace XDG_DATA_HOME=/workspace/.local/share XDG_CONFIG_HOME=/workspace/.config
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
This runtime uses MiSans fonts by Xiaomi.
|
||||
Official source: https://hyperos.mi.com/font/zh/download/
|
||||
Archive: https://hyperos.mi.com/font-download/MiSans.zip
|
||||
|
||||
MiSans is subject to the MiSans Fonts Intellectual Property License Agreement
|
||||
published on the official download page. It is not an open-source font license.
|
||||
Installation and use indicate acceptance of that agreement. Do not modify the
|
||||
fonts or distribute/sell them separately. Review the complete agreement before
|
||||
redistributing a runtime image containing these fonts.
|
||||
+1
-1
@@ -8,7 +8,7 @@ if [[ -f "${DSH_HOME}/profiles/web/package.json" ]]; then
|
||||
# pnpm otherwise probes registry metadata for the overlapping optional peer.
|
||||
# Restrict offline mode to profiles containing only our fixed archives.
|
||||
install_env=()
|
||||
if node -e 'const m=require(process.env.DSH_HOME+"/profiles/web/package.json"); const pins={"dsh-mgr-workspace":"file:/usr/local/lib/dsh-mgr/dsh-mgr-workspace-1.0.0.tgz","dsh-web-mobile":"file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.0.5.tgz","dsh-free-search":"file:/usr/local/lib/dsh-mgr/dsh-free-search-0.8.5.tgz"}; process.exit(Object.entries({...m.dependencies,...m.devDependencies,...m.optionalDependencies}).every(([n,v])=>pins[n]===v)?0:1)'; then
|
||||
if node -e 'const m=require(process.env.DSH_HOME+"/profiles/web/package.json"); const pins={"dsh-chat-manager":"file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.12-dsh-mgr.1.tgz","dsh-mgr-workspace":"file:/usr/local/lib/dsh-mgr/dsh-mgr-workspace-1.0.0.tgz","dsh-web-mobile":"file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.0.5.tgz","dsh-free-search":"file:/usr/local/lib/dsh-mgr/dsh-free-search-0.8.5.tgz"}; process.exit(Object.entries({...m.dependencies,...m.devDependencies,...m.optionalDependencies}).every(([n,v])=>pins[n]===v)?0:1)'; then
|
||||
install_env=(npm_config_offline=true)
|
||||
fi
|
||||
if timeout --signal=TERM --kill-after=5s 120s env CI=true "${install_env[@]}" dsh plugin --profile web install --ignore-scripts; then
|
||||
|
||||
@@ -14,22 +14,23 @@ if (!existsSync(profile) || readdirSync(profile).length === 0) {
|
||||
mkdirSync(profile, { recursive: true });
|
||||
writeFileSync(join(profile, 'package.json'), JSON.stringify({
|
||||
name: 'dsh-profile-web', private: true,
|
||||
dependencies: { 'dsh-mgr-workspace': 'file:/usr/local/lib/dsh-mgr/dsh-mgr-workspace-1.0.0.tgz', 'dsh-web-mobile': 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.0.5.tgz', 'dsh-free-search': 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.8.5.tgz' },
|
||||
dependencies: { 'dsh-chat-manager': 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.12-dsh-mgr.1.tgz', 'dsh-mgr-workspace': 'file:/usr/local/lib/dsh-mgr/dsh-mgr-workspace-1.0.0.tgz', 'dsh-web-mobile': 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.0.5.tgz', 'dsh-free-search': 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.8.5.tgz' },
|
||||
pnpm: { overrides: {
|
||||
'dsh-chat-manager>clsx': 'file:/usr/local/lib/dsh-mgr/clsx-2.1.1.tgz',
|
||||
'dsh-free-search>@deepseek-ai/schemastery': 'file:/usr/local/lib/dsh-mgr/schemastery-3.18.4.tgz',
|
||||
'@deepseek-ai/schemastery>@deepseek-ai/cosmokit': 'file:/usr/local/lib/dsh-mgr/cosmokit-1.8.5.tgz',
|
||||
'@deepseek-ai/schemastery>@standard-schema/spec': 'file:/usr/local/lib/dsh-mgr/spec-1.1.0.tgz',
|
||||
} },
|
||||
dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', 'dsh-web-mobile', 'dsh-free-search', 'dsh-mgr-workspace'] } },
|
||||
dsh: { profile: { bundles: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', 'dsh-web-mobile', 'dsh-free-search', 'dsh-mgr-workspace', 'dsh-chat-manager'] } },
|
||||
}, null, 2), { flag: 'wx', mode: 0o600 });
|
||||
console.log('dsh-mgr: new web profile includes [email protected] and [email protected]');
|
||||
console.log('dsh-mgr: new web profile includes [email protected] and [email protected] and [email protected]');
|
||||
}
|
||||
const manifestPath = join(profile, 'package.json');
|
||||
const workspacePath = join(profile, 'pnpm-workspace.yaml');
|
||||
// Backups omit workspace settings; restore host-provided peers before pnpm runs.
|
||||
if (existsSync(manifestPath) && !existsSync(workspacePath)) {
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
||||
if (manifest.dependencies?.['dsh-web-mobile'] === 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.0.5.tgz' || manifest.dependencies?.['dsh-free-search'] === 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.8.5.tgz') {
|
||||
if (manifest.dependencies?.['dsh-chat-manager'] === 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.12-dsh-mgr.1.tgz' || manifest.dependencies?.['dsh-web-mobile'] === 'file:/usr/local/lib/dsh-mgr/dsh-web-mobile-3.0.5.tgz' || manifest.dependencies?.['dsh-free-search'] === 'file:/usr/local/lib/dsh-mgr/dsh-free-search-0.8.5.tgz') {
|
||||
writeFileSync(workspacePath, 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n', { flag: 'wx', mode: 0o600 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ test('new profile enables bundle once and preserves existing configuration', ()
|
||||
run();
|
||||
const file=join(home,'profiles/web/package.json');
|
||||
const original=readFileSync(file,'utf8');
|
||||
assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-chat-manager'));
|
||||
assert.equal(JSON.parse(original).dependencies['dsh-chat-manager'], 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.12-dsh-mgr.1.tgz');
|
||||
assert.equal(JSON.parse(original).pnpm.overrides['dsh-chat-manager>clsx'], 'file:/usr/local/lib/dsh-mgr/clsx-2.1.1.tgz');
|
||||
assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-web-mobile'));
|
||||
assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-free-search'));
|
||||
assert.ok(JSON.parse(original).dsh.profile.bundles.includes('dsh-mgr-workspace'));
|
||||
@@ -24,11 +27,15 @@ test('new profile enables bundle once and preserves existing configuration', ()
|
||||
run();assert.equal(readFileSync(file,'utf8'),original);
|
||||
const legacy = JSON.parse(original);
|
||||
delete legacy.dependencies['dsh-mgr-workspace'];
|
||||
delete legacy.dependencies['dsh-chat-manager'];
|
||||
delete legacy.pnpm.overrides['dsh-chat-manager>clsx'];
|
||||
legacy.dsh.profile.bundles = legacy.dsh.profile.bundles.filter(name => name !== 'dsh-chat-manager');
|
||||
legacy.dsh.profile.bundles = legacy.dsh.profile.bundles.filter(name => name !== 'dsh-mgr-workspace');
|
||||
writeFileSync(file, JSON.stringify(legacy));
|
||||
run();
|
||||
assert.deepEqual(JSON.parse(readFileSync(file)), legacy);
|
||||
const disabled = JSON.parse(original);
|
||||
disabled.dsh.profile.bundles = disabled.dsh.profile.bundles.filter(name => name !== 'dsh-chat-manager');
|
||||
disabled.dsh.profile.bundles = disabled.dsh.profile.bundles.filter(name => name !== 'dsh-web-mobile');
|
||||
writeFileSync(file, JSON.stringify(disabled));
|
||||
writeFileSync(workspace, 'custom workspace');
|
||||
@@ -42,6 +49,23 @@ test('new profile enables bundle once and preserves existing configuration', ()
|
||||
writeFileSync(file,custom);run();assert.equal(readFileSync(file,'utf8'),custom);
|
||||
} finally { rmSync(home,{recursive:true,force:true}); }
|
||||
});
|
||||
test('chat-manager-only restored profile gets offline peer settings without manifest changes', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-chat-restore-'));
|
||||
try {
|
||||
const profile = join(home, 'profiles/web');
|
||||
mkdirSync(profile, { recursive: true });
|
||||
const manifest = JSON.stringify({ dependencies: { 'dsh-chat-manager': 'file:/usr/local/lib/dsh-mgr/dsh-chat-manager-1.5.12-dsh-mgr.1.tgz' } });
|
||||
writeFileSync(join(profile, 'package.json'), manifest);
|
||||
execFileSync(process.execPath, [new URL('./init-profile.mjs', import.meta.url).pathname], { env: { ...process.env, DSH_HOME: home } });
|
||||
assert.equal(readFileSync(join(profile, 'package.json'), 'utf8'), manifest);
|
||||
assert.match(readFileSync(join(profile, 'pnpm-workspace.yaml'), 'utf8'), /autoInstallPeers: false/);
|
||||
const entrypoint = readFileSync(new URL('./entrypoint.sh', import.meta.url), 'utf8');
|
||||
const predicate = entrypoint.match(/if node -e '([^']+)'/)[1];
|
||||
execFileSync(process.execPath, ['-e', predicate], { env: { ...process.env, DSH_HOME: home } });
|
||||
writeFileSync(join(profile, 'package.json'), JSON.stringify({ dependencies: { 'dsh-chat-manager': '^1.5.12' } }));
|
||||
assert.throws(() => execFileSync(process.execPath, ['-e', predicate], { env: { ...process.env, DSH_HOME: home }, stdio: 'pipe' }));
|
||||
} finally { rmSync(home, { recursive: true, force: true }); }
|
||||
});
|
||||
test('nonempty legacy profiles are not automatically injected', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-plugin-legacy-'));
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
import { patchClient } from './patch-chat-manager.mjs';
|
||||
|
||||
const root = '/usr/local/lib/dsh-mgr';
|
||||
const version = '1.5.12';
|
||||
const patchedVersion = '1.5.12-dsh-mgr.1';
|
||||
const hostManifest = '/usr/local/lib/node_modules/@deepseek-ai/dsh/package.json';
|
||||
const destination = '/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/dsh-chat-manager';
|
||||
const packages = [
|
||||
['dsh-chat-manager', version, 'https://registry.npmjs.org/dsh-chat-manager/-/dsh-chat-manager-1.5.12.tgz', 'UpecIKftHTKZMAdZg0w/GFtYGN2A/fB7NVQ/OrrBOKsq1frN5nm4CDBH8KZggwfT9GSx+MjwLlwYgxDfDWLm4g=='],
|
||||
['clsx', '2.1.1', 'https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz', 'eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=='],
|
||||
];
|
||||
mkdirSync(root, { recursive: true });
|
||||
for (const [name, release, url, integrity] of packages) {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(120000) });
|
||||
if (!response.ok) throw new Error('download failed: ' + name);
|
||||
const data = Buffer.from(await response.arrayBuffer());
|
||||
if (createHash('sha512').update(data).digest('base64') !== integrity) throw new Error('integrity mismatch: ' + name);
|
||||
writeFileSync(`${root}/${name}-${release}.tgz`, data, { mode: 0o644 });
|
||||
}
|
||||
mkdirSync(destination, { recursive: true });
|
||||
execFileSync('tar', ['-xzf', `${root}/dsh-chat-manager-${version}.tgz`, '--strip-components=1', '--no-same-owner', '-C', destination]);
|
||||
const pkg = JSON.parse(readFileSync(destination + '/package.json'));
|
||||
if (pkg.name !== 'dsh-chat-manager' || pkg.version !== version || pkg.license !== 'MIT' || JSON.stringify(pkg.dependencies) !== JSON.stringify({ clsx: '^2.0.0' })) throw new Error('unexpected chat-manager metadata');
|
||||
if (!readFileSync(destination + '/LICENSE', 'utf8').includes('MIT License')) throw new Error('missing chat-manager MIT license');
|
||||
const require = createRequire(hostManifest);
|
||||
const host = JSON.parse(readFileSync(hostManifest));
|
||||
const semver = createRequire('/usr/local/lib/node_modules/npm/package.json')('semver');
|
||||
for (const [name, range] of Object.entries(pkg.peerDependencies || {})) {
|
||||
if (name === 'react') {
|
||||
if (host.version !== '0.2.0-rc.2') throw new Error('unverified browser React host: ' + host.version);
|
||||
continue;
|
||||
}
|
||||
const peer = require(name + '/package.json');
|
||||
if (!semver.satisfies(peer.version, range)) throw new Error('incompatible chat-manager host dependency: ' + name + '@' + peer.version);
|
||||
}
|
||||
const client = destination + '/lib/client.js';
|
||||
writeFileSync(client, patchClient(readFileSync(client, 'utf8'), version));
|
||||
pkg.version = patchedVersion;
|
||||
writeFileSync(destination + '/package.json', JSON.stringify(pkg, null, 2));
|
||||
execFileSync('chmod', ['-R', 'a+rX', destination]);
|
||||
execFileSync('tar', ['-czf', `${root}/dsh-chat-manager-${patchedVersion}.tgz`, '--transform=s,^dsh-chat-manager,package,', '-C', destination + '/..', 'dsh-chat-manager']);
|
||||
execFileSync('chmod', ['644', `${root}/dsh-chat-manager-${patchedVersion}.tgz`]);
|
||||
const clsx = destination + '/node_modules/clsx';
|
||||
mkdirSync(clsx, { recursive: true });
|
||||
execFileSync('tar', ['-xzf', `${root}/clsx-2.1.1.tgz`, '--strip-components=1', '--no-same-owner', '-C', clsx]);
|
||||
execFileSync('chmod', ['-R', 'a+rX', clsx]);
|
||||
host.dependencies = { ...host.dependencies, 'dsh-chat-manager': patchedVersion };
|
||||
writeFileSync(hostManifest, JSON.stringify(host, null, 2));
|
||||
console.log('Installed dsh-chat-manager@' + patchedVersion + ' and [email protected] (SHA-512 verified; no lifecycle scripts; MIT retained)');
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
const url = 'https://hyperos.mi.com/font-download/MiSans.zip';
|
||||
const sha256 = 'b6aa1fc827035922612df8edf36e5609bca1c5441e25cd57572204569b7b81d9';
|
||||
const weights = ['Thin', 'ExtraLight', 'Light', 'Normal', 'Regular', 'Medium', 'Demibold', 'Semibold', 'Bold', 'Heavy'];
|
||||
const destination = '/usr/local/share/fonts/misans';
|
||||
const temporary = mkdtempSync(join(tmpdir(), 'misans-'));
|
||||
try {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(300000) });
|
||||
if (!response.ok) throw new Error(`MiSans download failed: ${response.status}`);
|
||||
const archive = Buffer.from(await response.arrayBuffer());
|
||||
if (createHash('sha256').update(archive).digest('hex') !== sha256) {
|
||||
throw new Error('MiSans archive checksum mismatch; review upstream changes before updating');
|
||||
}
|
||||
const archivePath = join(temporary, 'MiSans.zip');
|
||||
writeFileSync(archivePath, archive);
|
||||
mkdirSync(destination, { recursive: true });
|
||||
for (const weight of weights) {
|
||||
const name = `MiSans-${weight}.ttf`;
|
||||
// Extract only fixed, reviewed filenames; never unpack arbitrary archive paths.
|
||||
const font = execFileSync('python3', ['-c', 'import sys, zipfile; sys.stdout.buffer.write(zipfile.ZipFile(sys.argv[1]).read(sys.argv[2]))', archivePath, `MiSans/ttf/${name}`], { maxBuffer: 16000000 });
|
||||
writeFileSync(join(destination, name), font, { mode: 0o644 });
|
||||
}
|
||||
execFileSync('fc-cache', ['-f', destination], { stdio: 'inherit' });
|
||||
const family = execFileSync('fc-match', ['-f', '%{family}', 'MiSans'], { encoding: 'utf8' });
|
||||
if (!family.split(',').includes('MiSans')) throw new Error(`MiSans font lookup failed: ${family}`);
|
||||
} finally {
|
||||
rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Scoped to this plugin; host authentication and global fetch are unchanged.
|
||||
async function dshChatFetch(path, init) {
|
||||
const parts = window.location.pathname.split('/');
|
||||
const prefix = parts[1] === 'dsh' && parts[2] ? '/dsh/' + parts[2] : '';
|
||||
return fetch(prefix + path, init);
|
||||
}
|
||||
async function dshChatJson(response) {
|
||||
const text = await response.text();
|
||||
try { return JSON.parse(text); } catch {
|
||||
throw new Error('Chat Manager: HTTP ' + response.status + ' returned a non-JSON response; check the instance proxy path and authentication.');
|
||||
}
|
||||
}
|
||||
export const clientHelpers = dshChatFetch.toString() + String.fromCharCode(10) + dshChatJson.toString() + String.fromCharCode(10);
|
||||
|
||||
export function patchClient(source, version) {
|
||||
if (version !== '1.5.12') throw new Error('unsupported chat-manager version');
|
||||
if (source.includes('dshChatFetch') || source.split('await fetch(').length !== 4
|
||||
|| source.split('await response.json()').length !== 4
|
||||
|| source.split('/plugins/dsh-session-delete/').length !== 4) {
|
||||
throw new Error('unexpected chat-manager client; review upstream changes');
|
||||
}
|
||||
return clientHelpers + source.replaceAll('await fetch(', 'await dshChatFetch(')
|
||||
.replaceAll('await response.json()', 'await dshChatJson(response)');
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { runInNewContext } from 'node:vm';
|
||||
import { clientHelpers, patchClient } from './patch-chat-manager.mjs';
|
||||
|
||||
test('chat paths preserve instance prefix and POST options', async () => {
|
||||
for (const pathname of ['/dsh/demo/', '/dsh/demo', '/dsh/demo/session/123', '/', '/other/']) {
|
||||
let received;
|
||||
const context = { window: { location: { pathname } }, fetch: (...args) => { received = args; } };
|
||||
runInNewContext(clientHelpers, context);
|
||||
for (const action of ['delete', 'restore', 'archive-search']) {
|
||||
const path = '/plugins/dsh-session-delete/' + action;
|
||||
const options = { method: 'POST', body: '{}', headers: { 'x-dsh-session-delete-confirmation': 'delete-session' } };
|
||||
await context.dshChatFetch(path, options);
|
||||
assert.equal(received[0], (pathname.startsWith('/dsh/demo') ? '/dsh/demo' : '') + path);
|
||||
assert.equal(received[1], options);
|
||||
}
|
||||
}
|
||||
});
|
||||
test('non-JSON errors report HTTP status without exposing response content', async () => {
|
||||
const context = {};
|
||||
runInNewContext(clientHelpers, context);
|
||||
await assert.rejects(context.dshChatJson(new Response('method not allowed', { status: 405 })), /HTTP 405.*non-JSON/);
|
||||
const body = await context.dshChatJson(Response.json({ ok: false, error: { code: 'forbidden' } }, { status: 403 }));
|
||||
assert.equal(body.error.code, 'forbidden');
|
||||
});
|
||||
test('patch rejects unsupported or changed clients and double patching', () => {
|
||||
const source = Array(3).fill("await fetch('/plugins/dsh-session-delete/delete'); await response.json()").join(';');
|
||||
const patched = patchClient(source, '1.5.12');
|
||||
assert.equal(patched.split('await dshChatFetch(').length, 4);
|
||||
assert.throws(() => patchClient(source, '1.5.13'), /unsupported/);
|
||||
assert.throws(() => patchClient('', '1.5.12'), /unexpected/);
|
||||
assert.throws(() => patchClient(patched, '1.5.12'), /unexpected/);
|
||||
});
|
||||
@@ -14,3 +14,12 @@ test('Node and Caddy stages inherit the target platform', () => {
|
||||
assert.deepEqual(stages, ['FROM caddy:2.10.2 AS proxy', 'FROM node:24-bookworm-slim']);
|
||||
assert.ok(dockerfile.includes('COPY --from=proxy /usr/bin/caddy /usr/bin/caddy'));
|
||||
});
|
||||
|
||||
test('runtime includes Python tooling and headless LibreOffice with CJK fonts', () => {
|
||||
const dockerfile = readFileSync(new URL('./Dockerfile', import.meta.url), 'utf8');
|
||||
const install = dockerfile.split(String.fromCharCode(92, 10)).join(' ').split(String.fromCharCode(10)).find(line => line.startsWith('RUN apt-get update && apt-get install'));
|
||||
for (const pkg of ['python3', 'python3-pip', 'python3-venv', 'python-is-python3', 'libreoffice', 'fonts-noto-cjk']) {
|
||||
assert.ok(install.split(/\s+/).includes(pkg), `missing runtime package: ${pkg}`);
|
||||
}
|
||||
assert.ok(dockerfile.includes('RUN python --version && python3 -m pip --version && libreoffice --headless --version'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user