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
+Text[Citation here]
+[Citation] Named reference
+ Reuse reference
+{{Reflist}} Display footnotes
+```
+
+### Special Tags
+```wikitext
+[[escaped]] Disable markup
+
preformatted block
Preformatted (no markup)
+ Code highlighting
+code here
+
+ 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 citation[Source].
+
+=== 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: ''
+ 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
+Statement[Source information]
+
+== References ==
+{{Reflist}}
+```
+
+### Named References
+```wikitext
+First use[Smith, 2020, p. 42]
+Second use
+```
+
+### Grouped References
+```wikitext
+Note[Explanatory note]
+Source[Regular 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:
+Block:
+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 0000000..e856a04
Binary files /dev/null and b/.claude/skills/wiki-sync-translate/references/PatchString.txt differ
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}")