From c3e28bf7de4bc134d552e16073c5c74f83896805 Mon Sep 17 00:00:00 2001 From: wdjwxh Date: Sun, 22 Mar 2026 11:12:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=87=AA=E5=8A=A8=E7=BF=BB?= =?UTF-8?q?=E8=AF=91skill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 7 +- .claude/skills/mediawiki-wikitext/SKILL.md | 148 +++++++ .../mediawiki-wikitext/assets/snippets.yaml | 236 ++++++++++ .../mediawiki-wikitext/references/syntax.md | 345 +++++++++++++++ .../references/templates.md | 311 +++++++++++++ .claude/skills/wiki-sync-translate/SKILL.md | 139 ++++++ .../references/PatchString.txt | Bin 0 -> 162844 bytes .../wiki-sync-translate/scripts/wiki_sync.py | 417 ++++++++++++++++++ sync.py | 32 +- 9 files changed, 1615 insertions(+), 20 deletions(-) create mode 100644 .claude/skills/mediawiki-wikitext/SKILL.md create mode 100644 .claude/skills/mediawiki-wikitext/assets/snippets.yaml create mode 100644 .claude/skills/mediawiki-wikitext/references/syntax.md create mode 100644 .claude/skills/mediawiki-wikitext/references/templates.md create mode 100644 .claude/skills/wiki-sync-translate/SKILL.md create mode 100644 .claude/skills/wiki-sync-translate/references/PatchString.txt create mode 100644 .claude/skills/wiki-sync-translate/scripts/wiki_sync.py diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2c98cd5..5d05853 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,12 @@ "allow": [ "Bash(python:*)", "Bash(tree:*)", - "Bash(dir:*)" + "Bash(dir:*)", + "Bash(python3 -m venv venv)", + "Bash(source venv/bin/activate)", + "Bash(pip install:*)", + "Bash(source /mnt/d/code/sync-pd2-wiki/venv/bin/activate)", + "Bash(curl -s \"https://wiki.projectdiablo2.cn/w/api.php?action=query&prop=revisions&titles=Filter%20Info&rvprop=ids&format=json\")" ] } } diff --git a/.claude/skills/mediawiki-wikitext/SKILL.md b/.claude/skills/mediawiki-wikitext/SKILL.md new file mode 100644 index 0000000..89b1ab7 --- /dev/null +++ b/.claude/skills/mediawiki-wikitext/SKILL.md @@ -0,0 +1,148 @@ +--- +name: mediawiki-wikitext +description: MediaWiki Wikitext markup language for Wikipedia and wiki-based sites. Use when creating or editing wiki articles, generating wikitext content, working with wiki tables/templates/references, or converting content to wikitext format. Triggers on requests mentioning Wikipedia, MediaWiki, wikitext, wiki markup, or wiki article creation. +--- + +# MediaWiki Wikitext + +Generate and edit content using MediaWiki's wikitext markup language. + +## Quick Reference + +### Text Formatting +```wikitext +''italic'' '''bold''' '''''bold italic''''' +inline code subscript superscript +strikethrough underline +``` + +### Headings (line start only, avoid level 1) +```wikitext +== Level 2 == +=== Level 3 === +==== Level 4 ==== +``` + +### Lists +```wikitext +* Bullet item # Numbered item ; Term +** Nested ## Nested : Definition +``` + +### Links +```wikitext +[[Page Name]] Internal link +[[Page Name|Display Text]] With display text +[[Page Name#Section]] Section link +[https://url Display Text] External link +[[File:image.jpg|thumb|Caption]] Image +[[Category:Name]] Category (place at end) +``` + +### Table +```wikitext +{| class="wikitable" +|+ Caption +|- +! Header 1 !! Header 2 +|- +| Cell 1 || Cell 2 +|} +``` + +### Templates & Variables +```wikitext +{{TemplateName}} Basic call +{{TemplateName|arg1|name=value}} With arguments +{{{parameter|default}}} Parameter (in template) +{{PAGENAME}} {{CURRENTYEAR}} Magic words +``` + +### References +```wikitext +TextCitation here +Citation Named reference + Reuse reference +{{Reflist}} Display footnotes +``` + +### Special Tags +```wikitext +[[escaped]] Disable markup +
preformatted block
Preformatted (no markup) + Code highlighting +code here + +x^2 + y^2 = z^2 LaTeX math + Comment (hidden) +---- Horizontal rule +#REDIRECT [[Target Page]] Redirect (first line only) +``` + +### Magic Words +```wikitext +__NOTOC__ Hide table of contents +__TOC__ Position TOC here +__NOEDITSECTION__ Hide section edit links +``` + +## Common Patterns + +### Article Structure +```wikitext +{{Infobox Type +| name = Example +| image = Example.jpg +}} + +'''Article Title''' is a brief introduction. + +== Section == +Content with citationSource. + +=== Subsection === +More content. + +== See also == +* [[Related Article]] + +== References == +{{Reflist}} + +== External links == +* [https://example.com Official site] + +{{DEFAULTSORT:Sort Key}} +[[Category:Category Name]] +``` + +### Template Definition +```wikitext +{{Documentation}} +{| class="wikitable" +! {{{title|Default Title}}} +|- +| {{{content|No content provided}}} +{{#if:{{{footer|}}}| +{{!}}- +{{!}} {{{footer}}} +}} +|} + +``` + +## Key Syntax Rules + +1. **Headings**: Use `==` to `======`; don't use `=` (reserved for page title) +2. **Line-start markup**: Lists (`*#;:`), headings, tables (`{|`) must start at line beginning +3. **Closing tags**: Close heading equals on same line; no text after closing `==` +4. **Blank lines**: Create paragraph breaks; single newlines are ignored +5. **Pipes in templates**: Use `{{!}}` for literal `|` inside templates +6. **Escaping**: Use `` to escape markup; `&` for `&`, `<` for `<` + +## Resources + +For detailed syntax, see: +- **references/syntax.md**: Complete markup reference with all options +- **references/templates.md**: Template and parser function details +- **assets/snippets.yaml**: Editor snippets for common patterns diff --git a/.claude/skills/mediawiki-wikitext/assets/snippets.yaml b/.claude/skills/mediawiki-wikitext/assets/snippets.yaml new file mode 100644 index 0000000..ffe11c9 --- /dev/null +++ b/.claude/skills/mediawiki-wikitext/assets/snippets.yaml @@ -0,0 +1,236 @@ +# MediaWiki Wikitext Snippets +# For VS Code and compatible editors + +--- +# Headings +h2: + prefix: '@h2' + body: "== ${1:Heading} ==\n" + description: Level 2 heading + +h3: + prefix: '@h3' + body: "=== ${1:Heading} ===\n" + description: Level 3 heading + +h4: + prefix: '@h4' + body: "==== ${1:Heading} ====\n" + description: Level 4 heading + +# Text formatting +bold: + prefix: '@bold' + body: "'''${1:text}'''" + description: Bold text + +italic: + prefix: '@italic' + body: "''${1:text}''" + description: Italic text + +# Links +link: + prefix: '@link' + body: '[[${1:Page}]]' + description: Internal link + +linkd: + prefix: '@linkd' + body: '[[${1:Page}|${2:Display}]]' + description: Link with display text + +elink: + prefix: '@elink' + body: '[${1:https://} ${2:Text}]' + description: External link + +file: + prefix: '@file' + body: '[[File:${1:name.jpg}|thumb|${2:Caption}]]' + description: Image/file + +cat: + prefix: '@cat' + body: '[[Category:${1:Name}]]' + description: Category + +# Table +table: + prefix: '@table' + body: | + {| class="wikitable" + |+ ${1:Caption} + |- + ! ${2:Header1} !! ${3:Header2} + |- + | ${4:Cell1} || ${5:Cell2} + |} + description: Basic table + +tr: + prefix: '@tr' + body: | + |- + | ${1} || ${2} + description: Table row + +# References +ref: + prefix: '@ref' + body: '${1:Citation}' + description: Reference + +refn: + prefix: '@refn' + body: '${2:Citation}' + description: Named reference + +refr: + prefix: '@refr' + body: '' + description: Reference reuse + +reflist: + prefix: '@reflist' + body: | + == References == + {{Reflist}} + description: References section + +# Templates +tpl: + prefix: '@tpl' + body: '{{${1:Template}}}' + description: Template call + +tplp: + prefix: '@tplp' + body: '{{${1:Template}|${2:param}=${3:value}}}' + description: Template with params + +infobox: + prefix: '@infobox' + body: | + {{Infobox ${1:type} + | name = ${2} + | image = ${3} + }} + description: Infobox + +# Code +code: + prefix: '@code' + body: | + + ${2:code} + + description: Syntax highlight block + +codei: + prefix: '@codei' + body: '${1:code}' + description: Inline code + +nowiki: + prefix: '@nowiki' + body: '${1:text}' + description: Escape markup + +pre: + prefix: '@pre' + body: | +
+    ${1:text}
+    
+ description: Preformatted block + +math: + prefix: '@math' + body: '${1:formula}' + description: Math formula + +# Comments +comment: + prefix: '@comment' + body: '' + description: Comment + +todo: + prefix: '@todo' + body: '' + description: TODO comment + +# Magic words +notoc: + prefix: '@notoc' + body: '__NOTOC__' + description: Hide TOC + +toc: + prefix: '@toc' + body: '__TOC__' + description: TOC position + +# Lists +ul: + prefix: '@ul' + body: | + * ${1:Item 1} + * ${2:Item 2} + * ${3:Item 3} + description: Bullet list + +ol: + prefix: '@ol' + body: | + # ${1:Item 1} + # ${2:Item 2} + # ${3:Item 3} + description: Numbered list + +dl: + prefix: '@dl' + body: | + ; ${1:Term} + : ${2:Definition} + description: Definition list + +# Structure +redirect: + prefix: '@redirect' + body: '#REDIRECT [[${1:Target}]]' + description: Redirect + +article: + prefix: '@article' + body: | + {{Infobox + | name = ${1:Name} + }} + + '''${2:Title}''' is ${3:description}. + + == Overview == + ${4:Content} + + == References == + {{Reflist}} + + [[Category:${5:Category}]] + description: Article template + +hr: + prefix: '@hr' + body: '----' + description: Horizontal rule + +br: + prefix: '@br' + body: '
' + description: Line break + +sort: + prefix: '@sort' + body: '{{DEFAULTSORT:${1:Key}}}' + description: Default sort key diff --git a/.claude/skills/mediawiki-wikitext/references/syntax.md b/.claude/skills/mediawiki-wikitext/references/syntax.md new file mode 100644 index 0000000..fd609df --- /dev/null +++ b/.claude/skills/mediawiki-wikitext/references/syntax.md @@ -0,0 +1,345 @@ +# MediaWiki Wikitext Syntax Reference + +Complete syntax reference for MediaWiki wikitext markup. + +## Text Formatting (Inline) + +| Syntax | Result | Notes | +|--------|--------|-------| +| `''text''` | *italic* | Two single quotes | +| `'''text'''` | **bold** | Three single quotes | +| `'''''text'''''` | ***bold italic*** | Five single quotes | +| `text` | `monospace` | Inline code | +| `x` | Variable style | | +| `Ctrl` | Keyboard input | | +| `output` | Sample output | | +| `2` | Subscript | H₂O | +| `2` | Superscript | x² | +| `text` | ~~strikethrough~~ | | +| `text` | Deleted text | Semantic | +| `text` | Inserted text | Often underlined | +| `text` | Underline | | +| `text` | Small text | | +| `text` | Large text | Deprecated | + +## Headings + +```wikitext += Level 1 = (Don't use - reserved for page title) +== Level 2 == +=== Level 3 === +==== Level 4 ==== +===== Level 5 ===== +====== Level 6 ====== +``` + +**Rules:** +- Must start at line beginning +- No text after closing equals on same line +- 4+ headings auto-generate TOC (unless `__NOTOC__`) +- Spaces around heading text are optional but recommended + +## Lists + +### Unordered (Bullet) Lists +```wikitext +* Item 1 +* Item 2 +** Nested item 2.1 +** Nested item 2.2 +*** Deeper nesting +* Item 3 +``` + +### Ordered (Numbered) Lists +```wikitext +# First item +# Second item +## Sub-item 2.1 +## Sub-item 2.2 +### Sub-sub-item +# Third item +``` + +### Definition Lists +```wikitext +; Term 1 +: Definition 1 +; Term 2 +: Definition 2a +: Definition 2b +``` + +### Mixed Lists +```wikitext +# Numbered item +#* Bullet under numbered +#* Another bullet +# Next numbered +#: Definition-style continuation +``` + +### Indentation +```wikitext +: Single indent +:: Double indent +::: Triple indent +``` + +**Note:** List markers must be at line start. Blank lines end the list. + +## Links + +### Internal Links +```wikitext +[[Page Name]] +[[Page Name|Display Text]] +[[Page Name#Section]] +[[Page Name#Section|Display Text]] +[[Namespace:Page Name]] +[[/Subpage]] +[[../Sibling Page]] +``` + +### External Links +```wikitext +[https://example.com] Numbered link [1] +[https://example.com Display Text] Named link +https://example.com Auto-linked +``` + +### Special Links +```wikitext +[[File:Image.jpg]] Embed image +[[File:Image.jpg|thumb|Caption]] Thumbnail with caption +[[File:Image.jpg|thumb|left|200px|Caption]] +[[Media:File.pdf]] Direct file link +[[Category:Category Name]] Add to category +[[:Category:Category Name]] Link to category (no add) +[[Special:RecentChanges]] Special page +``` + +### Interwiki Links +```wikitext +[[en:English Article]] Language link +[[wikt:word]] Wiktionary +[[commons:File:Image.jpg]] Wikimedia Commons +``` + +## Images + +### Basic Syntax +```wikitext +[[File:Example.jpg|options|caption]] +``` + +### Image Options + +| Option | Description | +|--------|-------------| +| `thumb` | Thumbnail (default right-aligned) | +| `frame` | Framed, no resize | +| `frameless` | Thumbnail without frame | +| `border` | Thin border | +| `right`, `left`, `center`, `none` | Alignment | +| `200px` | Width | +| `x100px` | Height | +| `200x100px` | Max dimensions | +| `upright` | Smart scaling for tall images | +| `upright=0.5` | Custom ratio | +| `link=Page` | Custom link target | +| `link=` | No link | +| `alt=Text` | Alt text for accessibility | + +### Gallery +```wikitext + +File:Image1.jpg|Caption 1 +File:Image2.jpg|Caption 2 + + + +File:Image1.jpg +File:Image2.jpg + +``` + +## Tables + +### Basic Structure +```wikitext +{| class="wikitable" +|+ Caption +|- +! Header 1 !! Header 2 !! Header 3 +|- +| Cell 1 || Cell 2 || Cell 3 +|- +| Cell 4 || Cell 5 || Cell 6 +|} +``` + +### Table Elements + +| Markup | Location | Meaning | +|--------|----------|---------| +| `{|` | Start | Table start | +| `|}` | End | Table end | +| `|+` | After `{|` | Caption | +| `|-` | Row | Row separator | +| `!` | Cell | Header cell | +| `!!` | Cell | Header cell separator (same row) | +| `|` | Cell | Data cell | +| `||` | Cell | Data cell separator (same row) | + +### Cell Attributes +```wikitext +| style="background:#fcc" | Red background +| colspan="2" | Spans 2 columns +| rowspan="3" | Spans 3 rows +! scope="col" | Column header +! scope="row" | Row header +``` + +### Sortable Table +```wikitext +{| class="wikitable sortable" +|- +! Name !! Value +|- +| Alpha || 1 +| Beta || 2 +|} +``` + +## References + +### Basic Citation +```wikitext +StatementSource information + +== References == +{{Reflist}} +``` + +### Named References +```wikitext +First useSmith, 2020, p. 42 +Second use +``` + +### Grouped References +```wikitext +NoteExplanatory note +SourceRegular citation + +== Notes == +{{Reflist|group="note"}} + +== References == +{{Reflist}} +``` + +## Special Tags + +### nowiki (Escape Markup) +```wikitext +[[Not a link]] +<nowiki> Outputs: +``` + +### pre (Preformatted) +```wikitext +
+Preformatted text
+  Whitespace preserved
+  '''Markup not processed'''
+
+``` + +### syntaxhighlight (Code) +```wikitext + +def hello(): + print("Hello") + + + +# Line numbers starting at 10 + +``` + +Supported languages: python, javascript, php, java, c, cpp, csharp, ruby, perl, sql, xml, html, css, json, yaml, bash, etc. + +### math (LaTeX) +```wikitext +Inline: E = mc^2 +Block: \sum_{i=1}^n i = \frac{n(n+1)}{2} +Chemistry: H2O +``` + +### Transclusion Control +```wikitext +Only when transcluded +Only on template page itself +Only this part is transcluded +``` + +## HTML Entities + +| Entity | Character | Description | +|--------|-----------|-------------| +| `&` | & | Ampersand | +| `<` | < | Less than | +| `>` | > | Greater than | +| ` ` | (space) | Non-breaking space | +| `—` | — | Em dash | +| `–` | – | En dash | +| `→` | → | Right arrow | +| `←` | ← | Left arrow | +| `©` | © | Copyright | +| `€` | € | Euro | +| `:` | : | Colon (in definition lists) | + +## Miscellaneous + +### Horizontal Rule +```wikitext +---- +``` + +### Comments +```wikitext + + +``` + +### Line Breaks +```wikitext +Line 1
Line 2 +``` + +### Redirect +```wikitext +#REDIRECT [[Target Page]] +#REDIRECT [[Target Page#Section]] +``` +Must be first line of page. + +### Signatures (Talk pages) +```wikitext +~~~ Username only +~~~~ Username and timestamp +~~~~~ Timestamp only +``` + +### Categories +```wikitext +[[Category:Category Name]] +[[Category:Category Name|Sort Key]] +{{DEFAULTSORT:Sort Key}} +``` +Place at end of article. diff --git a/.claude/skills/mediawiki-wikitext/references/templates.md b/.claude/skills/mediawiki-wikitext/references/templates.md new file mode 100644 index 0000000..98cfc2c --- /dev/null +++ b/.claude/skills/mediawiki-wikitext/references/templates.md @@ -0,0 +1,311 @@ +# MediaWiki Templates and Parser Functions + +## Template Basics + +### Calling Templates +```wikitext +{{TemplateName}} +{{TemplateName|positional arg}} +{{TemplateName|param1=value1|param2=value2}} +{{TemplateName +| param1 = value1 +| param2 = value2 +}} +``` + +### Template Parameters (Definition Side) +```wikitext +{{{1}}} First positional parameter +{{{paramName}}} Named parameter +{{{1|default}}} With default value +{{{paramName|}}} Empty default (vs undefined) +``` + +### Transclusion +```wikitext +{{:Page Name}} Transclude article (with colon) +{{Template Name}} Transclude template +{{subst:Template Name}} Substitute (one-time expansion) +{{safesubst:Template}} Safe substitution +{{msgnw:Template}} Show raw wikitext +``` + +## Parser Functions + +### Conditionals + +#### #if (empty test) +```wikitext +{{#if: {{{param|}}} | not empty | empty or undefined }} +{{#if: {{{param|}}} | has value }} +``` + +#### #ifeq (equality test) +```wikitext +{{#ifeq: {{{type}}} | book | It's a book | Not a book }} +{{#ifeq: {{{1}}} | {{{2}}} | same | different }} +``` + +#### #iferror +```wikitext +{{#iferror: {{#expr: 1/0}} | Division error | OK }} +``` + +#### #ifexist (page exists) +```wikitext +{{#ifexist: Page Name | [[Page Name]] | Page doesn't exist }} +``` + +#### #ifexpr (expression test) +```wikitext +{{#ifexpr: {{{count}}} > 10 | Many | Few }} +{{#ifexpr: {{{year}}} mod 4 = 0 | Leap year candidate }} +``` + +#### #switch +```wikitext +{{#switch: {{{type}}} +| book = 📚 Book +| article = 📄 Article +| website = 🌐 Website +| #default = 📋 Other +}} + +{{#switch: {{{1}}} +| A | B | C = First three letters +| #default = Something else +}} +``` + +### String Functions + +#### #len +```wikitext +{{#len: Hello }} Returns: 5 +``` + +#### #pos (find position) +```wikitext +{{#pos: Hello World | o }} Returns: 4 (first 'o') +{{#pos: Hello World | o | 5 }} Returns: 7 (after position 5) +``` + +#### #sub (substring) +```wikitext +{{#sub: Hello World | 0 | 5 }} Returns: Hello +{{#sub: Hello World | 6 }} Returns: World +{{#sub: Hello World | -5 }} Returns: World (from end) +``` + +#### #replace +```wikitext +{{#replace: Hello World | World | Universe }} Returns: Hello Universe +``` + +#### #explode (split) +```wikitext +{{#explode: a,b,c,d | , | 2 }} Returns: c (third element) +``` + +#### #urlencode / #urldecode +```wikitext +{{#urlencode: Hello World }} Returns: Hello%20World +{{#urldecode: Hello%20World }} Returns: Hello World +``` + +### Math Functions + +#### #expr +```wikitext +{{#expr: 1 + 2 * 3 }} Returns: 7 +{{#expr: (1 + 2) * 3 }} Returns: 9 +{{#expr: 2 ^ 10 }} Returns: 1024 +{{#expr: 17 mod 5 }} Returns: 2 +{{#expr: floor(3.7) }} Returns: 3 +{{#expr: ceil(3.2) }} Returns: 4 +{{#expr: round(3.567, 2) }} Returns: 3.57 +{{#expr: abs(-5) }} Returns: 5 +{{#expr: sqrt(16) }} Returns: 4 +{{#expr: ln(e) }} Returns: 1 +{{#expr: sin(pi/2) }} Returns: 1 +``` + +**Operators:** `+`, `-`, `*`, `/`, `^` (power), `mod`, `round`, `floor`, `ceil`, `abs`, `sqrt`, `ln`, `exp`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `pi`, `e` + +**Comparison:** `=`, `<>`, `!=`, `<`, `>`, `<=`, `>=` + +**Logical:** `and`, `or`, `not` + +### Date/Time Functions + +#### #time +```wikitext +{{#time: Y-m-d }} Current: 2024-01-15 +{{#time: F j, Y | 2024-01-15 }} January 15, 2024 +{{#time: Y年n月j日 | 2024-01-15 }} 2024年1月15日 +{{#time: l | 2024-01-15 }} Monday +``` + +**Format codes:** +| Code | Output | Description | +|------|--------|-------------| +| Y | 2024 | 4-digit year | +| y | 24 | 2-digit year | +| n | 1 | Month (no leading zero) | +| m | 01 | Month (with leading zero) | +| F | January | Full month name | +| M | Jan | Abbreviated month | +| j | 5 | Day (no leading zero) | +| d | 05 | Day (with leading zero) | +| l | Monday | Full weekday | +| D | Mon | Abbreviated weekday | +| H | 14 | Hour (24h, leading zero) | +| i | 05 | Minutes (leading zero) | +| s | 09 | Seconds (leading zero) | + +#### #timel (local time) +```wikitext +{{#timel: H:i }} Local time +``` + +### Formatting Functions + +#### #formatnum +```wikitext +{{#formatnum: 1234567.89 }} 1,234,567.89 +{{#formatnum: 1,234.56 | R }} 1234.56 (raw) +``` + +#### #padleft / #padright +```wikitext +{{#padleft: 7 | 3 | 0 }} 007 +{{#padright: abc | 6 | . }} abc... +``` + +#### #lc / #uc / #lcfirst / #ucfirst +```wikitext +{{#lc: HELLO }} hello +{{#uc: hello }} HELLO +{{#lcfirst: HELLO }} hELLO +{{#ucfirst: hello }} Hello +{{lc: HELLO }} hello (shortcut) +``` + +### Other Functions + +#### #tag +```wikitext +{{#tag: ref | Citation text | name=smith }} +Equivalent to: Citation text +``` + +#### #invoke (Lua modules) +```wikitext +{{#invoke: ModuleName | functionName | arg1 | arg2 }} +``` + +## Magic Words + +### Behavior Switches +```wikitext +__NOTOC__ No table of contents +__FORCETOC__ Force TOC even with <4 headings +__TOC__ Place TOC here +__NOEDITSECTION__ No section edit links +__NEWSECTIONLINK__ Add new section link +__NONEWSECTIONLINK__ Remove new section link +__NOGALLERY__ No gallery in category +__HIDDENCAT__ Hidden category +__INDEX__ Index by search engines +__NOINDEX__ Don't index +__STATICREDIRECT__ Don't update redirect +``` + +### Page Variables +```wikitext +{{PAGENAME}} Page title without namespace +{{FULLPAGENAME}} Full page title +{{BASEPAGENAME}} Parent page name +{{SUBPAGENAME}} Subpage name +{{ROOTPAGENAME}} Root page name +{{TALKPAGENAME}} Associated talk page +{{NAMESPACE}} Current namespace +{{NAMESPACENUMBER}} Namespace number +{{PAGEID}} Page ID +{{REVISIONID}} Revision ID +``` + +### Site Variables +```wikitext +{{SITENAME}} Wiki name +{{SERVER}} Server URL +{{SERVERNAME}} Server hostname +{{SCRIPTPATH}} Script path +``` + +### Date/Time Variables +```wikitext +{{CURRENTYEAR}} 4-digit year +{{CURRENTMONTH}} Month (01-12) +{{CURRENTMONTHNAME}} Month name +{{CURRENTDAY}} Day (1-31) +{{CURRENTDAYNAME}} Day name +{{CURRENTTIME}} HH:MM +{{CURRENTTIMESTAMP}} YYYYMMDDHHmmss +``` + +### Statistics +```wikitext +{{NUMBEROFPAGES}} Total pages +{{NUMBEROFARTICLES}} Content pages +{{NUMBEROFFILES}} Files +{{NUMBEROFUSERS}} Registered users +{{NUMBEROFACTIVEUSERS}} Active users +{{NUMBEROFEDITS}} Total edits +{{PAGESINCATEGORY:Name}} Pages in category +``` + +## Template Examples + +### Simple Infobox +```wikitext +{{Documentation}} +{| class="infobox" style="width:22em" +|- +! colspan="2" style="background:#ccc" | {{{title|{{PAGENAME}}}}} +{{#if:{{{image|}}}| +{{!}}- +{{!}} colspan="2" {{!}} [[File:{{{image}}}|200px|center]] +}} +|- +| '''Type''' || {{{type|Unknown}}} +|- +| '''Date''' || {{{date|—}}} +|} + +``` + +### Navbox Template +```wikitext +{{Documentation}} +{| class="navbox" style="width:100%" +|- +! style="background:#ccf" | {{{title|Navigation}}} +|- +| {{{content|}}} +|} + +``` + +### Citation Template +```wikitext +{{#if:{{{author|}}}|{{{author}}}. }}{{#if:{{{title|}}}|''{{{title}}}''. }}{{#if:{{{publisher|}}}|{{{publisher}}}{{#if:{{{year|}}}|, }}}}{{{year|}}}.{{#if:{{{url|}}}| [{{{url}}} Link]}} +``` + +## Tips + +1. **Pipe trick**: `[[Help:Contents|]]` displays as "Contents" +2. **Escape pipes in templates**: Use `{{!}}` for literal `|` +3. **Trim whitespace**: Parameters automatically trim whitespace +4. **Check emptiness correctly**: `{{{param|}}}` vs `{{{param}}}` - the former has empty default, latter is undefined if not passed +5. **Subst for speed**: Use `{{subst:Template}}` for templates that don't need dynamic updates diff --git a/.claude/skills/wiki-sync-translate/SKILL.md b/.claude/skills/wiki-sync-translate/SKILL.md new file mode 100644 index 0000000..2bc5a5e --- /dev/null +++ b/.claude/skills/wiki-sync-translate/SKILL.md @@ -0,0 +1,139 @@ +--- +name: wiki-sync-translate +description: 同步英文 MediaWiki 页面变更到中文翻译文档。当用户需要更新中文 Wiki页面、同步英文变更、或翻译 Wiki 内容时触发。适用于 Project Diablo 2 Wiki 的中英双语同步维护场景。 +--- + +# Wiki 同步翻译 + +同步英文 Wiki 页面变更到中文翻译文档,保持行号一致。 + +## 使用方法 + +- 用户请求更新中文 Wiki 页面 +- 用户请求同步英文 Wiki 变更 +- 用户指定某个页面需要进行翻译更新 +- 用户执行 `/wiki-sync-translate <页面名称>` + +## 工作目录 + +脚本位于:`.claude/skills/wiki-sync-translate/scripts/wiki_sync.py` + +## 执行步骤 + +### Step 1: 运行同步脚本 + +使用 skill 目录下的专用脚本获取变更: + +```bash +cd /mnt/d/code/sync-pd2-wiki +source venv/bin/activate +python .claude/skills/wiki-sync-translate/ +scripts/wiki_sync.py --title "<页面名称>" --since <上次同步时间> --run +``` + +参数说明: +- `--title`: 指定要同步的页面名称 +- `--since`: 起始时间,格式如 `2026-01-02T12:07:05Z` +- `--run`: 必须提供此参数才会执行 + +### Step 2: 读取输出文件 + +脚本会在 `wiki_sync_output/<时间戳>/changed_pages/` 目录下生成: + +| 文件 | 说明 | 用途 | +|------|------|------| +| `*.comparison.json` | 结构化变更信息 | **AI 读取,包含行号和变更内容** | +| `*.full.txt` | 英文最新版本 | 参考对照 | +| `*.cn.txt` | 中文原文 | **基于此文件进行修改** | +| `*.old.txt` | 英文历史版本 | 参考对照 | + + +### Step 3: 解析 comparison.json +`comparison.json` 格式: + +```json +{ + "title": "页面标题", + "has_cn_translation": true, + "summary": { + "total_changes": 1, + "replaced": 1, + "added": 0, + "removed": 0 + }, + "changes": [ + { + "type": "replaced", + "old_line": 66, + "new_line": 66, + "old_content": "旧内容", + "new_content": "新内容" + } + ] +} +``` + +变更类型: +- `replaced`: 替换,`old_line` 表示需要修改的行号 +- `added`: 新增,`new_line` 表示插入位置 +- `removed`: 删除,`old_line` 表示要删除的行 + +### Step 4: 更新中文文档 + +**核心原则:行号必须完全一致** + +1. 创建 `wiki_sync_output/<时间戳>/result_pages/` 目录(如不存在) +2. 复制 `*.cn.txt` 内容到 `result_pages/<页面名>.cn.txt` +3. 根据 `changes` 中的行号定位对应行 +4. 智能更新: + - 仅同步变更的内容 + - 保留中文翻译(如 "赛季 12" 不改为 "Season 12") + - 新增的英文内容智能翻译成中文,可以用`grep` 命令在`references/PatchString.txt` 搜索技能或物品的中文名称,搜索不到的则智能翻译按照暗黑破坏神2的常用命名。 + - 保持 MediaWiki 语法正确,语法可以参照`mediawiki-wikitest` skill. +5. 保存到 `wiki_sync_output/<时间戳>/result_pages/<页面名>.cn.txt` + +### Step 5: 输出结果 + +将更新后的文档保存到 `wiki_sync_output/<时间戳>/result_pages/<页面名>.cn.txt`,用户可直接复制到 Wiki。 + + ## 示例 + +**输入 - comparison.json:** +```json +{ + "changes": [{ + "type": "replaced", + "old_line": 66, + "old_content": "| style=\"...\"| 2025-11-25
(Season 12)", + "new_content": "| style=\"...\"| 2026-01-25
(Season 12)" + }] +} +``` + +**中文原文第66行:** +``` +| style="color:#3f6e2d; background-color:#161f0c; border-color:#0d1709"| 2025-11-25
(赛季 12) +``` +**更新后第66行:** +``` +| style="color:#3f6e2d; background-color:#161f0c; border-color:#0d1709"| 2026-01-25
(赛季 12) +``` + +**变更说明:** 日期 `2025-11-25` → `2026-01-25`,但 `赛季 12` 保持中文不变。 + + + ## 注意事项 + +1. **行号一致性**:确保中英文文档行号完全对应,这是长期维护的基础 +2. **保留翻译**:只同步变更内容,不替换已有的中文翻译 +3. **MediaWiki 语法**: + - 表格分隔符 `|-` 位置保持一致 + - 链接格式 `[[页面名|显示文本]]` 不变 + - 样式属性 `style="..."` 不变 +4. **特殊字符**:注意 `
`、` ` 等 HTML 实体 + +## 错误处理 + +- 如果 `has_cn_translation` 为 false,提示用户该页面无中文翻译 +- 如果 `is_new_page` 为 true,说明是新页面,需要全新翻译 +- 如果找不到对应行,可能是中英文版本不同步,需要人工确认 \ No newline at end of file diff --git a/.claude/skills/wiki-sync-translate/references/PatchString.txt b/.claude/skills/wiki-sync-translate/references/PatchString.txt new file mode 100644 index 0000000000000000000000000000000000000000..e856a04dc5a01393a6da0588deafd81de12a233c GIT binary patch literal 162844 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 z z?!k=mqh+@2dZOxnbNgeQE-lm7aZcSOHO=N6T@21^uYFctslO=kS^N!dHdyN=Te-e@n02})cB-9Ka=XusVl6b-WoEg z{z&%y9fi7^X6`JiIP5EjZeATIPxf^j3JdF~mb*uxJ>d}s7Ig$42 zO+LC<&ox`-wI0d(py=nl4duy-!@k;1H@9rPx8e2CBiX_h<%K(GmmPmTQm6^N7vQNw zN3!4F=YwKJ7q%VAKAQ1C(bb{wh5bILW^-<@dSU;80t?i$=P&PgzNe6?W=;mSA844@ zN?*#il^G@*oUIwy`R5~rF!VGt(|z48bAx`=l4ckk4Qzj}M;i3Aqur;w8Rp)E9qm3j z(akCK7p*Il0==B(shiF$+YiZ|%L9cq({s4XxKdYWmhWnlZd=0ZNk<49jDgrJMhuQ{i#5P~jN zrw+6f`ZtYlmxc8`g_7&Jz=7?LX8ce*%`}l-t_v<`&soMoR;MsPiJ_S;p5~TK|hwp|_c3?lqg& zTBsX6G1a%j*8}uS)T5bst%W^cwy~3ynet>|Tj&WOY}se&dIIRvqvzE9+T6z(wTanA zGq0|Ce@CHSW*d9?A8kH<*vogjvNh+`oM!B-OU75USSO>jzJ542xM1_=vgaKe*nVLf zuS9awI`;!})Arn;*5I!)r`EOXGSdbZnkbVmSbib3o7+#X@Kc)+IBGNGrIbVfE>_zm zv>5pdsfC&ErJ<(x79$mymJYu|2Q29D;vVb>l|!G`@Mg2!(?(A-z~)uB_jg2mmx zMBsWmr@lLy2>x;;*Z7`>uWr^GUb#n^8;<+T4X515gw@a5J!tx0wM5@UH?0DP)*5qX zt9$2|du1QnR~}wq`o~5o`SF%BHtntFMWl;cjt+32Hn*4}#U~7(o?xiyO1U@x+ie}4 z<>7Gml~R|Ipj=*KsR?$~J~Z~S3zY6|go+=N7ntrD@=>p=%(PMN26;K;_2Kl^e;nAF znc6Zn`)bn*BQNZKt9GHhQ4)mNw!vIQ>MyEvj-lNAoSNE7tM3_t;-w*bpG$jB-8!}T zykqEB%a!(KTaeseerj`l-Hjo8t?vrIot~eeMo0#wrnYq(2Xj#k{9xa`*{Am|8*-Y6 zz7W(cZIvNMCJU9%|Ry`h&{RLt*=&(?{TUk+JcVX=z?3 zayU=6zst0lVuaFKO?gx{xAk_$vA8e3JrC<6)%y

q^HIQn8u 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 G9Qi*i4}ZG= literal 0 HcmV?d00001 diff --git a/.claude/skills/wiki-sync-translate/scripts/wiki_sync.py b/.claude/skills/wiki-sync-translate/scripts/wiki_sync.py new file mode 100644 index 0000000..a0f3a30 --- /dev/null +++ b/.claude/skills/wiki-sync-translate/scripts/wiki_sync.py @@ -0,0 +1,417 @@ +# -*- coding: utf-8 -*- +""" +MediaWiki Wiki 同步工具 - AI Agent 版本 +输出 JSON 格式的对比文件,便于 AI Agent 读取和处理 +""" + +import os +import argparse +from pathlib import Path +from datetime import datetime, timedelta +import requests +from dotenv import load_dotenv +import difflib +import json +import re + +# ==================== 配置区 ==================== +load_dotenv() +WIKI_API_URL_EN = os.getenv("WIKI_API_URL_EN", "https://wiki.projectdiablo2.com/w/api.php") +WIKI_API_URL_CN = os.getenv("WIKI_API_URL_CN", "https://wiki.projectdiablo2.cn/w/api.php") +OUTPUT_DIR = Path("wiki_sync_output") +OUTPUT_DIR.mkdir(exist_ok=True) + +CURRENT_OUTPUT_DIR = None +LAST_TIMESTAMP_FILE = "last_sync_timestamp.txt" + +SESSION_EN = requests.Session() +SESSION_EN.headers.update({ + "User-Agent": "WikiSyncTool/5.0 (AI Agent Version)" +}) + +SESSION_CN = requests.Session() +SESSION_CN.headers.update({ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" +}) +SESSION_CN.trust_env = False +# ================================================ + +def load_last_timestamp(): + if os.path.exists(LAST_TIMESTAMP_FILE): + with open(LAST_TIMESTAMP_FILE, encoding="utf-8") as f: + return f.read().strip() + return None + +def save_last_timestamp(ts): + with open(LAST_TIMESTAMP_FILE, "w", encoding="utf-8") as f: + f.write(ts) + +def get_recent_changes(since): + """获取自 since 时间后每个页面的最新 revid""" + params = { + "action": "query", + "list": "recentchanges", + "rcprop": "title|ids|timestamp", + "rctype": "edit|new", + "rcdir": "newer", + "rcstart": since, + "rclimit": 500, + "format": "json" + } + latest = {} + while True: + try: + r = SESSION_EN.get(WIKI_API_URL_EN, params=params) + r.raise_for_status() + data = r.json() + if "error" in data: + raise Exception(data["error"]) + for rc in data.get("query", {}).get("recentchanges", []): + latest[rc["title"]] = (rc["revid"], rc["timestamp"]) + if "continue" not in data: + break + params.update(data["continue"]) + except Exception as e: + print(f"获取最近更改时出错: {e}") + break + return latest + +def get_old_revid(title, end_time): + """获取指定时间前的最后一个 revid""" + params = { + "action": "query", + "prop": "revisions", + "titles": title, + "rvprop": "ids|timestamp", + "rvlimit": 1, + "rvdir": "older", + "rvstart": end_time, + "format": "json" + } + try: + r = SESSION_EN.get(WIKI_API_URL_EN, params=params).json() + pages = r["query"]["pages"] + page = next(iter(pages.values())) + if "revisions" in page: + return page["revisions"][0]["revid"] + except Exception as e: + print(f"获取旧版本ID时出错: {e}") + return None + +def get_page_content(wiki_url, session, title, revid=None): + """获取页面完整内容""" + params = { + "action": "query", + "prop": "revisions", + "titles": title, + "rvprop": "content|timestamp|ids", + "rvslots": "main", + "format": "json" + } + if revid: + params["rvstartid"] = revid + params["rvendid"] = revid + + try: + r = session.get(wiki_url, params=params).json() + pages = r["query"]["pages"] + page = next(iter(pages.values())) + if "revisions" in page: + rev = page["revisions"][0] + return rev["slots"]["main"]["*"], rev["timestamp"], rev["revid"] + except Exception as e: + print(f"获取页面内容时出错: {e}") + return None, None, None + +def generate_text_diff(old_text, new_text): + """生成 unified diff 格式""" + if not old_text: + return "新创建页面" + + old_lines = old_text.splitlines(keepends=True) + new_lines = new_text.splitlines(keepends=True) + + return ''.join(difflib.unified_diff(old_lines, new_lines, lineterm='\n')) + +def parse_diff_to_changes(diff_text): + """ + 解析 diff 文本,提取结构化的变更信息 + 返回一个列表,每个元素包含:变更类型、行号、旧内容、新内容 + """ + if not diff_text or diff_text.startswith("新创建页面"): + return [] + + changes = [] + current_old_line = 0 + current_new_line = 0 + in_hunk = False + + for line in diff_text.splitlines(): + if line.startswith('@@'): + match = re.match(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@', line) + if match: + current_old_line = int(match.group(1)) + current_new_line = int(match.group(3)) + in_hunk = True + elif line.startswith('---') or line.startswith('+++'): + continue + elif in_hunk: + if line.startswith('-'): + changes.append({ + "type": "removed", + "old_line": current_old_line, + "new_line": None, + "old_content": line[1:], + "new_content": None + }) + current_old_line += 1 + elif line.startswith('+'): + changes.append({ + "type": "added", + "old_line": None, + "new_line": current_new_line, + "old_content": None, + "new_content": line[1:] + }) + current_new_line += 1 + elif line.startswith(' '): + current_old_line += 1 + current_new_line += 1 + + return changes + +def group_changes_by_line(changes): + """ + 将变更按行号分组,将连续的删除和添加合并为替换操作 + """ + # 先收集所有的删除和添加 + removed_by_line = {} # old_line -> content + added_by_line = {} # new_line -> content + + for c in changes: + if c["type"] == "removed": + removed_by_line[c["old_line"]] = c["old_content"] + elif c["type"] == "added": + added_by_line[c["new_line"]] = c["new_content"] + + # 尝试将删除和添加配对 + grouped = [] + used_added = set() + + for old_line, old_content in sorted(removed_by_line.items()): + # 找一个未使用的添加行来配对 + paired = False + for new_line, new_content in sorted(added_by_line.items()): + if new_line not in used_added: + grouped.append({ + "type": "replaced", + "old_line": old_line, + "new_line": new_line, + "old_content": old_content, + "new_content": new_content + }) + used_added.add(new_line) + paired = True + break + + if not paired: + grouped.append({ + "type": "removed", + "old_line": old_line, + "new_line": None, + "old_content": old_content, + "new_content": None + }) + + # 添加未配对的新增行 + for new_line, new_content in sorted(added_by_line.items()): + if new_line not in used_added: + grouped.append({ + "type": "added", + "old_line": None, + "new_line": new_line, + "old_content": None, + "new_content": new_content + }) + + # 按行号排序 + grouped.sort(key=lambda x: x["old_line"] or x["new_line"] or 0) + + return grouped + +def create_diff_json(title, en_old_content, en_new_content, cn_content): + """ + 创建结构化的 JSON 对比数据(仅包含英文变更,AI自行匹配中文) + """ + # 生成英文 diff + diff_text = generate_text_diff(en_old_content, en_new_content) + + # 解析变更 + raw_changes = parse_diff_to_changes(diff_text) + grouped_changes = group_changes_by_line(raw_changes) + + # 构建输出结构(精简版,不含中文行内容) + result = { + "title": title, + "timestamp": datetime.now().isoformat(), + "is_new_page": diff_text == "新创建页面", + "has_cn_translation": cn_content is not None, + "summary": { + "total_changes": len(grouped_changes), + "replaced": len([c for c in grouped_changes if c["type"] == "replaced"]), + "added": len([c for c in grouped_changes if c["type"] == "added"]), + "removed": len([c for c in grouped_changes if c["type"] == "removed"]) + }, + "changes": grouped_changes + } + + return result + +def save_files(title, diff_json, en_full_text, cn_content, timestamp, revid=None, old_full_text=None): + """保存文件""" + global CURRENT_OUTPUT_DIR + + if CURRENT_OUTPUT_DIR is None: + current_time_str = datetime.now().strftime("%Y%m%d_%H%M%S") + CURRENT_OUTPUT_DIR = OUTPUT_DIR / current_time_str + CURRENT_OUTPUT_DIR.mkdir(exist_ok=True) + (CURRENT_OUTPUT_DIR / "new_pages").mkdir(exist_ok=True) + (CURRENT_OUTPUT_DIR / "changed_pages").mkdir(exist_ok=True) + print(f"创建输出目录: {CURRENT_OUTPUT_DIR}") + + safe_title = "".join(c if c.isalnum() or c in " -_." else "_" for c in title) + time_str = timestamp[:19].replace("-", "").replace(":", "").replace("T", "_") + base_filename = f"{safe_title}-{time_str}-{revid}" if revid else f"{safe_title}-{time_str}" + + is_new_page = diff_json["is_new_page"] + + if is_new_page: + target_dir = CURRENT_OUTPUT_DIR / "new_pages" + print(f" 检测到新页面") + + # 保存英文完整内容 + full_file = target_dir / f"{base_filename}.full.txt" + with open(full_file, "w", encoding="utf-8") as f: + f.write(en_full_text) + print(f" → 已保存: {full_file.relative_to(OUTPUT_DIR)}") + + else: + target_dir = CURRENT_OUTPUT_DIR / "changed_pages" + + # 保存英文完整内容 + full_file = target_dir / f"{base_filename}.full.txt" + with open(full_file, "w", encoding="utf-8") as f: + f.write(en_full_text) + print(f" → 已保存: {full_file.relative_to(OUTPUT_DIR)}") + + # 保存中文内容 + if cn_content: + cn_file = target_dir / f"{base_filename}.cn.txt" + with open(cn_file, "w", encoding="utf-8") as f: + f.write(cn_content) + print(f" → 已保存: {cn_file.relative_to(OUTPUT_DIR)}") + + # 保存 JSON 对比文件(核心输出) + json_file = target_dir / f"{base_filename}.comparison.json" + with open(json_file, "w", encoding="utf-8") as f: + json.dump(diff_json, f, ensure_ascii=False, indent=2) + print(f" → 已保存: {json_file.relative_to(OUTPUT_DIR)} (AI Agent 对比文件)") + + # 保存历史版本 + if old_full_text: + old_file = target_dir / f"{base_filename}.old.txt" + with open(old_file, "w", encoding="utf-8") as f: + f.write(old_full_text) + print(f" → 已保存: {old_file.relative_to(OUTPUT_DIR)}") + +def process_single_page(title, since_time, update_timestamp=False): + """处理单个页面""" + print(f"正在处理页面:{title}") + + # 获取最新内容 + latest_content, latest_ts, latest_revid = get_page_content(WIKI_API_URL_EN, SESSION_EN, title) + if latest_content is None: + print("页面不存在或被删除") + return None + + # 获取旧版本 + old_revid = get_old_revid(title, since_time) + old_content = None + + if old_revid: + old_content, _, _ = get_page_content(WIKI_API_URL_EN, SESSION_EN, title, old_revid) + if old_content is None: + print(" 无法获取历史版本,视为新页面") + + # 获取中文翻译 + print(" 搜索中文翻译...") + cn_content = None + + # 直接尝试获取同名页面 + cn_result, _, _ = get_page_content(WIKI_API_URL_CN, SESSION_CN, title) + if cn_result: + cn_content = cn_result + print(f" 找到中文页面 ({len(cn_content)} 字符)") + else: + print(" 未找到中文翻译") + + # 生成对比 JSON + diff_json = create_diff_json(title, old_content, latest_content, cn_content) + + print(f" 变更统计: 替换={diff_json['summary']['replaced']}, " + f"新增={diff_json['summary']['added']}, 删除={diff_json['summary']['removed']}") + + # 保存文件 + save_files(title, diff_json, latest_content, cn_content, latest_ts, latest_revid, old_content) + + if update_timestamp: + save_last_timestamp(latest_ts) + print(f"已更新时间戳 → {latest_ts}") + + return latest_ts + +def process_all_pages_since(since_time): + """处理所有变更页面""" + print("正在获取最近变更列表...") + changes = get_recent_changes(since_time) + + if not changes: + print("没有发现任何变更") + return + + latest_global_ts = since_time + for title, (revid, ts) in changes.items(): + print(f"\n处理:{title}") + page_ts = process_single_page(title, since_time) + if page_ts and page_ts > latest_global_ts: + latest_global_ts = page_ts + + save_last_timestamp(latest_global_ts) + print(f"\n同步完成!最新时间戳: {latest_global_ts}") + print(f"文件保存在: {CURRENT_OUTPUT_DIR.resolve() if CURRENT_OUTPUT_DIR else OUTPUT_DIR.resolve()}") + +def main(): + parser = argparse.ArgumentParser(description="MediaWiki 同步工具 - AI Agent 版本") + parser.add_argument("--since", type=str, help="起始时间,格式: 2025-11-28T00:00:00Z") + parser.add_argument("--title", type=str, help="只处理指定页面") + parser.add_argument("--update-timestamp", action="store_true", help="更新全局时间戳") + parser.add_argument("--run", action="store_true", help="执行同步") + + args = parser.parse_args() + + if not args.run: + parser.print_help() + return + + since_time = args.since or load_last_timestamp() + if not since_time: + since_time = (datetime.utcnow() - timedelta(days=1)).isoformat(timespec='seconds') + "Z" + print(f"起始时间: {since_time}") + + if args.title: + process_single_page(args.title.strip(), since_time, args.update_timestamp) + else: + process_all_pages_since(since_time) + +if __name__ == "__main__": + main() diff --git a/sync.py b/sync.py index cf189ad..17504bc 100644 --- a/sync.py +++ b/sync.py @@ -41,8 +41,9 @@ SESSION_EN.headers.update({ SESSION_CN = requests.Session() SESSION_CN.headers.update({ - "User-Agent": "WikiSyncTool/4.0 (your-email@example.com; MediaWiki Sync Bot)" + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" }) +SESSION_CN.trust_env = False # 禁用环境变量代理 # ================================================ def load_last_timestamp(): @@ -264,31 +265,24 @@ def parse_diff_with_line_numbers(diff_text): def search_chinese_page(title): """在中文wiki中搜索对应的页面""" - # 首先尝试精确匹配 + # 首先尝试直接获取页面(因为中文wiki禁用了标题搜索) params = { "action": "query", - "list": "search", - "srsearch": f'"{title}"', - "srwhat": "title", - "srlimit": 5, + "prop": "revisions", + "titles": title, + "rvprop": "ids", "format": "json" } try: - r = SESSION_CN.get(WIKI_API_URL_CN, params=params).json() - search_results = r.get("query", {}).get("search", []) + r = SESSION_CN.get(WIKI_API_URL_CN, params=params, timeout=10) + data = r.json() + pages = data.get("query", {}).get("pages", {}) - if search_results: - # 返回第一个匹配的结果 - return search_results[0]["title"] - - # 如果精确匹配没有结果,尝试模糊搜索 - params["srsearch"] = title.replace(" ", "%20") - r = SESSION_CN.get(WIKI_API_URL_CN, params=params).json() - search_results = r.get("query", {}).get("search", []) - - if search_results: - return search_results[0]["title"] + for page_id, page_info in pages.items(): + # page_id 为负数表示页面不存在 + if page_id != "-1" and "missing" not in page_info: + return page_info.get("title") except Exception as e: print(f"搜索中文页面时出错: {e}")