- Главная
- Плагины
- Безопасность и права доступа
- chinese-script-policy
chinese-script-policy
ksf1216/chinese-script-policy
Harness-neutral Traditional Chinese enforcer + offline converter (skill, DSH bundle, CLI). One script axis either way + Cantonese/Japanese-only filters. Converts both ways; optional wording preference. Also: an LLM output guard, cp950/GBK console scan.
Установка
dsh plugin --profile web add github:ksf1216/chinese-script-policyREADME
chinese-script-policy
Enforce the Chinese script you asked for — Traditional by default — on the Chinese you produce and store, and convert it fully offline.
GitHub · 中文說明 · Offline page · npm · CLI reference
One body of code, harness-neutral: the same repository is a DSH bundle, an Agent Skill (Claude Code and compatible harnesses), an npm package, a command-line tool, and a library for web applications. There is no separate "DSH build" and "generic build", so the two cannot drift apart.
The check is one script axis with two directions — use one at a time — plus two independent filter axes:
- Script axis (two directions, pick one): ask for Traditional and it reports Simplified-only glyphs (2,637); ask for Simplified and it reports Traditional-only glyphs (3,083). You name the target, and the tool reports what does not belong to it.
- Filter axes (each its own switch): Cantonese colloquial register, and Japanese-only characters and words (367 shinjitai and kokuji, plus 123 Japanese words). The Japanese axis catches text that looks Chinese but is Japanese — neither Traditional nor Simplified, so a Simplified-only table never sees it. Neither filter axis depends on which direction the script axis uses.
Conversion has three independently runnable steps: Traditional ↔ Simplified, Cantonese colloquial → written Chinese (only the parts that can never be written Chinese), and Japanese shinjitai → Chinese. All three run only when you ask for them.
An optional wording layer (--wording, one checkbox on the web page, off by default) decides whether a conversion also follows the target script's local vocabulary: Simplified → Traditional uses the Traditional preference table (軟件 → 軟體, 硬盤 → 硬碟); Traditional → Simplified uses the Simplified preference table (軟體 → 软件, 網路 → 网络). The direction picks the table, so the two cannot be mixed up. With the switch off you get pure glyph conversion (軟件 is correct Traditional on its own).
Zero runtime dependencies: the tables are compiled into checked-in JSON, so nothing pulls OpenCC in and dependencies stays empty. But "zero dependencies" does not mean "bare only" — the difference is when you use it and whether anything has to be installed first:
| When you want it | How | Install first? |
|---|---|---|
| Convert one document, or the machine has no Node and you would rather not open a terminal | Download dist/tradzh.html and double-click it (single file, offline) | No |
| You already have this directory (cloned, or installed as a skill) | node scripts/tradzh.js … directly | No |
| A permanent CLI, or use inside scripts and CI | npx chinese-script-policy …, or npm i -g chinese-script-policy (bins: tradzh, chinese-script) | Optional |
| DSH: write guard + GUI switches + skill registration | dsh plugin --profile web add chinese-script-policy | Yes |
| Claude Code / another harness, for the skill or the hook | Clone into that harness's skills directory, or take hooks.json | Clone only |
| A web application (server-side check or convert, or checking live in the browser) | Server: chinese-script-policy/lib; browser: dist/tradzh.html or bundle core | Server yes, browser no |
In other words, only the two "keep it running" paths need an install (the DSH plugin, and importing the library server-side). The single-file page and running the repo's own scripts need nothing, and npx fetches the package itself.
This is a tool for projects that must tell Traditional Chinese, Simplified Chinese, Cantonese colloquial writing and Japanese kanji apart. It does not claim that any one script is the correct one.
Highlights
- Report what does not belong to the script you asked for — on the command line, in the browser, or before a file is ever written
- Two independent filters on top: Cantonese colloquial register, and Japanese-only characters and words
- Convert Traditional ↔ Simplified, Cantonese → written Chinese, and Japanese shinjitai → Chinese, each step on its own
- One single-file offline page: no Node, no network, nothing uploaded, the original text never modified
- Zero runtime dependencies — the tables are checked-in JSON, so results do not change with the machine
- One implementation behind the CLI, the write hook, the DSH plugin and the web page, so they cannot disagree
- Reads UTF-8, BOM, UTF-16, Big5 and GB18030 by itself, and refuses to guess silently when it cannot tell
- Guards a local LLM's answers through a proxy, using that same decision function
Defaults
Checking — the script axis is one axis with two directions (you name the target; the tool reports what does not belong to it) and only one direction is ever on, next to two independent axes for Cantonese and Japanese:
| Front end | Script axis (pick one) | Cantonese / register axis | Japanese axis |
|---|---|---|---|
| Offline page | Two checkboxes: Traditional (report Simplified glyphs) ✅ on by default / Simplified (report Traditional glyphs) ⬜ off by default | ✅ on | ✅ on |
| CLI | --variant traditional (default, reports Simplified glyphs) / --variant simplified (reports Traditional glyphs) | needs --written | needs --japanese |
| Write hook (Claude Code format) | fixed to "ask for Traditional" (blocks Simplified glyphs) — it has no Simplified side | ✅ | ✅ |
| DSH plugin (card on the Plugins page) | one of three: ask for Traditional (default) / ask for Simplified / do not check | each can be turned off | each can be turned off |
⚠️ Never turn both directions on: feeding Traditional text (後面的軟件很乾淨) to the "ask for Simplified" side reports 3 glyphs (U+5F8C U+8EDF U+6DE8) — with both on, every Chinese document is blocked.
That is to say: the CLI checks only the Traditional direction of the script axis by default, and Cantonese and Japanese have to be asked for; the web page turns on the Traditional direction plus Cantonese plus Japanese (the Simplified direction stays off); the hook fixes the script axis to "ask for Traditional" and applies both filters with it; and the DSH plugin lets you pick one of the three on its card (on the Plugins page).
Conversion — the direction, and every step that can change meaning, are yours to ask for; only the two steps that leave the text itself unchanged are automatic:
| Step | Flag (CLI) | Other front ends | Default | Why |
|---|---|---|---|---|
| Script, Traditional ↔ Simplified | --to-traditional / --to-simplified | Web: two buttons; library: toTraditional / toSimplified | you pick a direction | The two directions produce completely different text; there is no sensible default |
| Register, Cantonese → written Chinese | --to-written | Web: one button; library: toWritten | off | Register is style; only the parts that can never appear in written Chinese are converted, and the rest is left to a model |
| Japanese, shinjitai → Chinese | --convert-japanese | Web: a "strip Japanese" checkbox; library: stripJapanese | off | A document may quote Japanese on purpose; a quotation should not be edited behind your back |
| Wording preference (local vocabulary) | --wording | Web: a "wording preference" checkbox; library: the 4th argument of toTraditional / toSimplified | off | 軟件 and 軟體 are both correct Chinese; swapping the word is a preference, not a correction. The direction picks the table (Simplified → Traditional uses the Traditional preference table, Traditional → Simplified the Simplified one), so they cannot be mixed up |
Glyph preference (裏面 → 裡面) | (no CLI flag) | automatic on all three (useTc is on by default) | always automatic | Two common ways to write the same character, collapsed to the common one; nothing about the meaning changes. To turn it off, call toTraditional(text, true, false) |
| Compatibility ideograph normalization | (no flag) | automatic on all three | always automatic | The text itself is unchanged, only the code point is standardized; there is nothing to ask about |
Install
Three shortest paths
# 1) Install nothing: download dist/tradzh.html and double-click it. Offline, no Node, no network.
# 2) Command line, nothing kept around (npx fetches it)
npx chinese-script-policy --dir .
npx chinese-script-policy --text "<paste a Simplified string>" --to-traditional
# 3) DSH: one command installs the plugin (write guard + skill registration + GUI switches)
dsh plugin --profile web add chinese-script-policy
DSH
As a local skill (simplest) — put the directory under $DSH_HOME/skills/, and the directory name must be chinese-script-policy:
git clone https://github.com/KSF1216/chinese-script-policy.git "$env:USERPROFILE\.dsh\skills\chinese-script-policy"
A new session sees it (DSH watches the directory live; no restart needed).
As a DSH bundle — one plugin line, whose plugin registers SKILL.md into the skill registry with ctx.skills.register(...) (what DSH calls embedded skills), so nothing has to be copied into ~/.dsh/skills:
dsh plugin --profile web add chinese-script-policy # published on npm
dsh plugin --profile web add github:KSF1216/chinese-script-policy
dsh plugin --profile web add ./chinese-script-policy-1.4.0.tgz
Installing straight from GitHub makes pnpm ask for an
allowBuildsentry in that profile'spnpm-workspace.yaml(which amounts to allowing this package's code to run at install time). Use npm or the tarball if you would rather not be asked.
Two DSH 0.1.7 details decide whether it works at all. The row this bundle inserts carries
id: chinese-script-policy, and on 0.1.7 that entry id IS the settings namespace: the card asks the host for the form of exactly that entry, so a row without anidcan never be edited from the GUI. And the host half imports@deepseek-ai/schemasteryto declare itsConfigschema, which a linked install (dsh plugin add <directory>) can only resolve if the package declares it — hence thepeerDependenciesentry inpackage.json. Without it the host half fails to load, that row never becomes active, and the guard dies without a sound (the skill goes with it).
One install affects one profile.
webandheadlesseach have their owndsh.profile.bundles, so run it once per shell you want guarded (change--profile).headlesshas no GUI, so a settings card there is meaningless — set the switches in that profile's owncordis.patch.ymlinstead; the guard itself works the same (the sametools/pre-execute). ⚠️ A card write lands in the active profile'scordis.patch.yml($DSH_HOME/profiles/<name>/), so the switches are per profile: flipping one in the GUI changes the profile you are running in, and the other keeps its own value. (The old$DSH_HOME/settings.yamlno longer holds plugin settings — 0.1.7 imports that document once and renames it tosettings.yaml.imported. On upgrade, achinese-script-policy:block there is imported into the entry of the same id, which is why the schema still accepts the old boolean spelling ofscript, so a profile patch written by 1.0–1.1 keeps loading.)
Check the install instead of trusting it (
npm run test:boot, i.e.node dev\dsh-boot-check.mjs): it boots a throwaway DSH with its own$DSH_HOMEand port — your~/.dshis not touched — and asserts three things: nothing was parked at boot,settings/describereally serves our namespace, and the served client bundle evaluates and registers our module.--keepkeeps the scratch directory;--url http://127.0.0.1:3099 --home <scratch home>attaches to an instance that is already running.
Other harnesses and the command line
SKILL.md is the generic Agent Skills format (YAML frontmatter plus Markdown), so any harness can read it directly:
git clone https://github.com/KSF1216/chinese-script-policy.git "$env:USERPROFILE\.claude\skills\chinese-script-policy"
git clone https://github.com/KSF1216/chinese-script-policy.git ./skills/chinese-script-policy
It also works as a command-line tool on its own (bins: tradzh, chinese-script):
node scripts\tradzh.js --dir . # check a whole directory tree
node scripts\tradzh.js --fix --to-traditional --write out.txt < in.txt
node scripts\tradzh.js --text "后面的软件很干净" --to-traditional # convert a string directly # simplified-example
node scripts\tradzh.js --japanese --dir . # check for Japanese-only characters
node scripts\tradzh.js --encoding FILE.md # report the encoding
The complete feature × command × explanation table (including --wording, --to-written, --convert-japanese and their defaults) is in references/cli.md.
Web applications
Any online system can use it, because all you need is somewhere JS runs: the browser itself, or a Node process.
| Your system | Which files | How it goes in |
|---|---|---|
| Front end, download and run, no build step | dist/tradzh.html (engine and all nine tables inlined) | Double-click it, or <iframe src="tradzh.html">. No Node needed |
| Front end inside your own app (check as you type) | scripts/core.js plus scripts/*.json | import { createCore } from 'chinese-script-policy/core' and inject the tables yourself (there is no fs) |
| Server on Node (Express / Next / Nuxt / Workers) | scripts/lib.js (reads its own tables) | import { scanText, toTraditional, guardInspect } from 'chinese-script-policy/lib' |
| Server not on JS (PHP / Python / Java) | scripts/tradzh.js | Call it as a subprocess, or hand the work to the browser page |
| Do not use | index.mjs (the DSH plugin), lib/client.js (the browser half of the DSH card) | The latter touches window on import and throws window is not defined under Node |
// server: check and convert (ESM and CJS both work)
import { guardInspect, toTraditional } from 'chinese-script-policy/lib';
const verdict = guardInspect(userText); // exactly the same decision as the DSH write hook
if (verdict) return res.status(422).json({ reason: verdict.reason });
const stored = toTraditional(userText); // conversion is a SEPARATE step; nothing is silently rewritten
// front end / Edge (no fs): the same engine, tables injected by you
import { createCore } from 'chinese-script-policy/core';
import simplifiedOnly from 'chinese-script-policy/tables/simplified-only' with { type: 'json' };
const core = createCore({ simplifiedOnly /* …the other eight */ });
⚠️ An ESM JSON import must carry
with { type: 'json' }, or Node throwsERR_IMPORT_ATTRIBUTE_MISSING(CJSrequire()does not need it). ⚠️core's named exports come from thescripts/core.mjsshim (the UMD wrapper hides them from Node's static analysis), andtest:apiasserts that the shim exports exactly the keys ofcore.js.
A working demo is included: examples/web-app/ is a zero-dependency Node HTTP server plus one front-end page, with guardInspect wired to the API boundary. Measured against the package as installed from npm:
| Request | Response |
|---|---|
GET / | 200 |
POST /api/check with Simplified text | 422 plus the raw reason (BLOCKED … U+8F6F U+51C0) |
POST /api/check with Traditional text | 200 {"clean":true} |
POST /api/convert to=traditional | 200 {"text":"後面的軟件很乾淨"} |
Write-time guard (three ways to install it)
The pre-write check is one decision (guardInspect / guardMessage in scripts/lib.js); the ways to install it differ only in how it is attached to the harness:
| Installation | Who it is for | Switches and configuration |
|---|---|---|
| DSH plugin (recommended) | DSH | The card on the Plugins page: enabled, one of three script choices, the register and Japanese switches, block versus warn, Windows script file types (.ps1 / .cmd, below) — applied the moment you save, with no restart (the fields are volatile, so an accepted change is written into the running configuration instead of remounting the plugin). It is a per-profile card: it writes the profile's own cordis.patch.yml, so web and headless can hold different answers; each profile needs its own install, and headless has no GUI, so its switches go in that file by hand |
| Claude Code / other harnesses | Any harness speaking the same hook protocol | Use this package's hooks.json (PreToolUse with matcher write|edit) |
| Environments without hooks | Everything else | Re-check yourself after writing with node scripts\tradzh.js <file>, and put the policy in the system prompt |
Besides Chinese characters, Windows script file types carry two more write rules: .ps1 must be pure ASCII, and .cmd / .bat must be pure ASCII with CRLF — because PowerShell 5.1 and cmd.exe both read scripts as ANSI, and UTF-8 Chinese without a BOM turns a .ps1 into a syntax error. The rule is in SKILL.md under "file-type traps"; the cause and the measurements are in references/encoding.md. The DSH plugin warns only by default for these writes; its card can change that to "block" or "off" — but "block" only ever applies to a .ps1 that would really break, while .cmd / .bat (which usually still run) always warn.
The details (the full hooks.json, why pluginRoot must be given, and four things you can achieve without editing the tables) are in references/integration.md.
Output guard: local LLM answers (a proxy)
The write guard covers files; when the bad characters are written by a model (the common case with a local model), they have to be stopped somewhere else. llama-server has no plugin mechanism (measured on build 10964: no plugin, hook or middleware in --help), and its two sampling-layer knobs cannot replace a rule: --logit-bias only lowers the probability of particular tokens (it would need the token ids of 2,637 characters, BPE merges characters into multi-character tokens, and it cannot express a phrase), and --grammar (GBNF) is impractical for free prose. So the mechanical layer goes outside the server:
npm run llm-guard-proxy # an alias, from this repo's directory
node examples\llm-proxy\llm-guard-proxy.mjs # equivalent; package installs use this path
That npm script is only an alias for the line below it; to pass arguments use
npm run llm-guard-proxy -- --port 8090(only what follows--is forwarded). It is deliberately not calledproxy:npm run proxyreads like it touches npm's own HTTP proxy settings, and this package has two guards (the write one and the output one), soproxywould not say which layer.
| Front end | What to do |
|---|---|
| SillyTavern / any OpenAI-compatible front end | Point the API base at http://127.0.0.1:8081/v1 |
| llama-server's built-in page | Open http://127.0.0.1:8081/ (the proxy covers the whole origin, so that UI works as-is) |
| An agent harness | ⚠️ Do not point it here — a response carrying tool_calls is never rewritten, and a proxy does not belong in the path of tool calls |
It is non-streaming (a request carrying stream:true is rewritten to false before it reaches the upstream), it uses the same guardInspect, a clean response passes through byte for byte, only the generation endpoints' JSON is read, and anything left that could not be converted is written to the log. Measured (2026-09, a local 27B model): asked directly it answered 用干净的语言。; through the proxy that becomes 用乾淨的語言。.
Why this exists
It started with knowledge management. The same Chinese material arrives from several places: Traditional you typed yourself, Simplified you pasted, and text a model generated (sometimes mixed with Japanese kanji). They look identical and are different strings, so deduplication, search and citation treat them as two records. Each source produces its own kind of inconsistency:
- Different source → different string:
軟體and软件are two different things to string equality, so deduplication and citation go wrong with them. - Models mix scripts: a local model (Qwen3.8), measured, held "always Traditional" across a long answer and still leaked
听and灵 长(2 runs out of 2). - And there are a third and a fourth kind: Japanese shinjitai (
竜発図) and compatibility ideographs — one character with two code points, indistinguishable to the eye.
So "which way of writing it" becomes something checkable, convertible and configurable rather than something a person has to remember: store one script (Traditional) plus normalization; use a slug or a UUID where an ASCII key is needed, and store the content as written (romanizing Chinese data loses proper nouns and full-text search on the original).
And why not simply trust the AI's own judgement — an AI's Traditional/Simplified judgement is a fuzzy impression, not a table lookup, so it makes two kinds of mistake: it reads characters shared by both scripts (您, 什麼, 可以, 我) as Simplified, and going Simplified → Traditional it picks the wrong candidate (头发 → 頭发).
Sneakier still are the traps in the tables themselves, which this project has hit and fixed twice:
- Variant preference: OpenCC holds that the Traditional form of
群is羣, while the standard form in the Ministry of Education dictionary is群 - Standard Traditional characters that Big5 also encodes:
峰 床 痴 秘 灶 粽 肴 虱 霉were wrongly listed as Simplified, so 「起床」「秘密」「玉山主峰」 were all reported as Simplified
The same lesson appeared a third time on the Japanese axis: of OpenCC's 403 shinjitai, 57 (峰 群 床 才 予 岳 連 衛 …) are in fact legitimate Traditional characters, and all of them are excluded — otherwise 「玉山主峰」 would be reported as Japanese. selftest.js has a case that joins those 57 characters into a single line and requires it to stay clean.
What OpenCC provides, and what this project adds
The relationship first, because it is exactly what makes this package worth using:
Almost all conversion data comes from OpenCC — measured, 66,769 of 66,884 entries (99.83%) are OpenCC's curation of more than ten years (Apache-2.0), and by entry count the phrase tables are most of it. That number is computed, not typed in: npm run stats counts every table and lists the original part entry by entry.
The 115 original entries (not OpenCC data) fall into two groups: 94 on the Cantonese register axis (17 characters + 30 phrases + 15 weak phrases + 3 word-order patterns + 12 convertible characters + 17 convertible phrases) and 21 kokuji — the latter from Japanese Wikipedia, because OpenCC has no kokuji at all (働 畑 辻 峠 凪, characters Japan coined and Chinese never had; see references/japanese.md).
But OpenCC is a converter, and six layers here are not its:
| # | Layer | Why OpenCC does not have it, or cannot replace it |
|---|---|---|
| 1 | Detection (two script directions + two filters) | OpenCC converts input to output; it has no "does this document use the wrong script" mode. The scanning, the per-line reporting, the waivers and the counts are all written here |
| 2 | Auditing, so that detection does not cry wolf | The naive approach is to use OpenCC's tables as the detection list — which reports 峰 床 痴 秘 灶 粽 肴 虱 霉 as Simplified (「起床」「秘密」「玉山主峰」 all hit) and 峰 群 床 才 as Japanese. This project suppresses the false alarms with a per-character cp950 / GB2312 audit plus a reviewed list, and pins the result with tests |
| 3 | The Cantonese register axis | OpenCC does not do Cantonese colloquial writing at all (its s2hk / t2hk handle Hong Kong's character variants, not 「嘅」 → 「的」). These 17 characters and 45 phrases are original data from this project |
| 4 | The encoding layer | Choosing between Big5 and GB18030 is decided by "how many of the decoded characters does this project's own table recognize" (correct 100%, wrong 54%); characters falling into the Private Use Area through HKSCS produce a loud warning and a failing check; Japanese and Korean files are positively identified and reported as "not a Chinese file" instead of being forced into mojibake. OpenCC does not deal with encoding |
| 5 | Integration with agents | The PreToolUse guard that runs before a write (hooks.json, which also works in Claude Code format), the DSH skill and bundle, the single-file offline HTML, and a full test suite (including the rule that a broken hook is silent, so it has to be tested). OpenCC has none of this |
| 6 | The wording preference layer | OpenCC keeps it inside the s2twp configuration (which has to be compiled and installed). This project compiles those 830 entries into a built-in table and makes it an option (--wording, one checkbox on the page), so the Traditional preference is available without OpenCC — and it is the same installed or not: the output does not change with the machine (see references/conversion.md) |
One more key difference: zero runtime dependencies. dependencies: {} — the tables are compiled into checked-in JSON, so even if OpenCC disappeared tomorrow, or the machine has neither network nor Node (use the offline HTML), the tool still runs. OpenCC is needed only to regenerate the tables, and the source URLs and rebuild commands are in the documentation. Licensing and derivation notices are in THIRD-PARTY-NOTICES.md.
Design principles
- One implementation only:
scripts/lib.jsis the single core, and the CLI (tradzh.js), the hook (pre-write-check.js, for Claude Code and similar) and the DSH plugin's guard (index.mjs) all call the sameguardInspect, so the three cannot disagree — even the "what to say when blocking" text is oneguardMessage. There used to be a Node and a PowerShell implementation side by side; the behaviour drifted and cost a lot of time. - Tables, not impressions: every decision is checked against a verified table, never extrapolated.
- The encoding is decided here: reading always determines the encoding from the bytes (UTF-8 / BOM / UTF-16 / Big5 / GB18030), and writing is always UTF-8 without a BOM, via a temporary file and a rename. It does not take the shell's or the editor's default.
- When it cannot tell, it says so: when a file is recognizable as Japanese (Shift-JIS), Korean (EUC-KR) or a Western / Cyrillic single-byte encoding, the tool says "not a Chinese file"; a file it cannot recognize at all is warned about and fails the check rather than quietly skipped — "never checked" must not look like "checked and clean".
Documentation
The README keeps only the shortest paths; everything else is there when you need it (all shipped files, all in this repository):
| What you want | Where |
|---|---|
| The complete feature × command × explanation table, and building or hosting the offline page | references/cli.md, references/offline-page.md |
| How to attach it to a harness (hook, DSH plugin, alternatives) | references/integration.md |
| Where the conversion tables come from, how wording preference works, why local edits survive a rebuild | references/conversion.md |
The glyph-table audit history (why 峰 床 痴 秘 灶 粽 cannot be listed as Simplified) | references/glyph-table.md |
| Encoding (UTF-8 / Big5 / HKSCS / PUA) and detection rewrites | references/encoding.md |
| The Cantonese register axis (four detection layers, weak-phrase rules) | references/cantonese.md |
| The Japanese axis (shinjitai, kokuji, Japanese words) | references/japanese.md |
| What each data file holds | references/data-files.md |
| 中文說明 — the same document in Chinese | README.zh.md |
| Maintainers: rebuilding the tables, running the tests, the release process | PUBLISHING.md |
Project status
This is an independent project. It is not an official DeepSeek product, and it is not affiliated with the OpenCC maintainers; DeepSeek, OpenCC and related names and marks belong to their respective owners. Everything runs offline and nothing is uploaded anywhere.
License
- Code: MIT, see
LICENSE. - The data tables under
scripts/are derived from OpenCC (Apache-2.0); their sources, the modifications, and how to regenerate them are inTHIRD-PARTY-NOTICES.md.
Похожие плагины
dsh-infinite-gen-4
minglink/dsh-infinite-gen-4
api-relay-audit
toby-bridges/api-relay-audit
dsh-redteam-model
seaof0/dsh-redteam-model
dsh-auto-review
perrylink/dsh-auto-review