5 İşlemeler 4ca7bf06fe ... bbc01c8d9a

Yazar SHA1 Mesaj Tarih
  malk bbc01c8d9a chore(docs): 规范文档与 OpenSpec 工作流迁出仓库,本仓只保留程序 1 hafta önce
  malk 9444e46cf5 docs(openspec): 立项 define-customer-tiering 客户接入三档分流 1 hafta önce
  malk 7af06b8a49 docs(mjava-com): 外部对接指南 + HMAC 签名算法 + Python/Node.js 样例 1 hafta önce
  malk 012df8fbd6 feat(mjava-pro): DynamicDDService / DynamicYDService 多租户凭据中转 1 hafta önce
  malk e93d5b7f5f docs(openspec): archive standardize-client-service-layering 1 hafta önce
71 değiştirilmiş dosya ile 206 ekleme ve 5180 silme
  1. 0 152
      .claude/commands/opsx/apply.md
  2. 0 157
      .claude/commands/opsx/archive.md
  3. 0 173
      .claude/commands/opsx/explore.md
  4. 0 106
      .claude/commands/opsx/propose.md
  5. 0 156
      .claude/skills/openspec-apply-change/SKILL.md
  6. 0 114
      .claude/skills/openspec-archive-change/SKILL.md
  7. 0 288
      .claude/skills/openspec-explore/SKILL.md
  8. 0 110
      .claude/skills/openspec-propose/SKILL.md
  9. 13 48
      CLAUDE.md
  10. 17 179
      README.md
  11. 94 0
      mjava-pro/src/main/java/com/malk/pro/service/aliwork/DynamicYDService.java
  12. 82 0
      mjava-pro/src/main/java/com/malk/pro/service/dingtalk/DynamicDDService.java
  13. 0 131
      openspec/BACKLOG.md
  14. 0 93
      openspec/changes/add-mjava-com/design.md
  15. 0 53
      openspec/changes/add-mjava-com/proposal.md
  16. 0 75
      openspec/changes/add-mjava-com/specs/baas-gateway/spec.md
  17. 0 48
      openspec/changes/add-mjava-com/specs/caller-registry/spec.md
  18. 0 53
      openspec/changes/add-mjava-com/tasks.md
  19. 0 75
      openspec/changes/add-mjava-pro/design.md
  20. 0 57
      openspec/changes/add-mjava-pro/proposal.md
  21. 0 61
      openspec/changes/add-mjava-pro/specs/multi-tenant-runtime/spec.md
  22. 0 54
      openspec/changes/add-mjava-pro/specs/tenant-registry/spec.md
  23. 0 50
      openspec/changes/add-mjava-pro/tasks.md
  24. 0 2
      openspec/changes/add-observability-foundation/.openspec.yaml
  25. 0 51
      openspec/changes/add-observability-foundation/design.md
  26. 0 36
      openspec/changes/add-observability-foundation/proposal.md
  27. 0 42
      openspec/changes/add-observability-foundation/specs/observability-foundation/spec.md
  28. 0 51
      openspec/changes/add-observability-foundation/tasks.md
  29. 0 2
      openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/.openspec.yaml
  30. 0 29
      openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/design.md
  31. 0 30
      openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/proposal.md
  32. 0 18
      openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/specs/crypto-utils/spec.md
  33. 0 12
      openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/tasks.md
  34. 0 130
      openspec/changes/archive/2026-04-19-add-request-auth-replay-guard/design.md
  35. 0 64
      openspec/changes/archive/2026-04-19-add-request-auth-replay-guard/proposal.md
  36. 0 58
      openspec/changes/archive/2026-04-19-add-request-auth-replay-guard/specs/replay-guard/spec.md
  37. 0 62
      openspec/changes/archive/2026-04-19-add-request-auth-replay-guard/specs/request-auth/spec.md
  38. 0 59
      openspec/changes/archive/2026-04-19-add-request-auth-replay-guard/tasks.md
  39. 0 119
      openspec/changes/archive/2026-04-19-extend-dingtalk-contacts-api/design.md
  40. 0 44
      openspec/changes/archive/2026-04-19-extend-dingtalk-contacts-api/proposal.md
  41. 0 95
      openspec/changes/archive/2026-04-19-extend-dingtalk-contacts-api/specs/dingtalk-contacts-v2/spec.md
  42. 0 71
      openspec/changes/archive/2026-04-19-extend-dingtalk-contacts-api/tasks.md
  43. 0 92
      openspec/changes/archive/2026-04-19-extend-yida-api-coverage/design.md
  44. 0 55
      openspec/changes/archive/2026-04-19-extend-yida-api-coverage/proposal.md
  45. 0 68
      openspec/changes/archive/2026-04-19-extend-yida-api-coverage/specs/yida-form-atomic/spec.md
  46. 0 75
      openspec/changes/archive/2026-04-19-extend-yida-api-coverage/specs/yida-process-atomic/spec.md
  47. 0 86
      openspec/changes/archive/2026-04-19-extend-yida-api-coverage/tasks.md
  48. 0 43
      openspec/changes/archive/2026-04-19-init-project-baseline/design.md
  49. 0 43
      openspec/changes/archive/2026-04-19-init-project-baseline/proposal.md
  50. 0 41
      openspec/changes/archive/2026-04-19-init-project-baseline/spec.md
  51. 0 28
      openspec/changes/archive/2026-04-19-init-project-baseline/tasks.md
  52. 0 86
      openspec/changes/archive/2026-04-26-add-integration-user-api/design.md
  53. 0 37
      openspec/changes/archive/2026-04-26-add-integration-user-api/proposal.md
  54. 0 85
      openspec/changes/archive/2026-04-26-add-integration-user-api/specs/integration-user-api/spec.md
  55. 0 46
      openspec/changes/archive/2026-04-26-add-integration-user-api/tasks.md
  56. 0 84
      openspec/changes/rename-dingtalk-impl-suffix/design.md
  57. 0 73
      openspec/changes/rename-dingtalk-impl-suffix/proposal.md
  58. 0 44
      openspec/changes/rename-dingtalk-impl-suffix/specs/client-service-layering/spec.md
  59. 0 52
      openspec/changes/rename-dingtalk-impl-suffix/tasks.md
  60. 0 91
      openspec/changes/standardize-client-service-layering/design.md
  61. 0 60
      openspec/changes/standardize-client-service-layering/proposal.md
  62. 0 196
      openspec/changes/standardize-client-service-layering/specs/client-service-layering/spec.md
  63. 0 44
      openspec/changes/standardize-client-service-layering/tasks.md
  64. 0 22
      openspec/specs/crypto-utils/spec.md
  65. 0 99
      openspec/specs/dingtalk-contacts-v2/spec.md
  66. 0 85
      openspec/specs/integration-user-api/spec.md
  67. 0 58
      openspec/specs/project-baseline.md
  68. 0 62
      openspec/specs/replay-guard/spec.md
  69. 0 66
      openspec/specs/request-auth/spec.md
  70. 0 72
      openspec/specs/yida-form-atomic/spec.md
  71. 0 79
      openspec/specs/yida-process-atomic/spec.md

+ 0 - 152
.claude/commands/opsx/apply.md

@@ -1,152 +0,0 @@
----
-name: "OPSX: Apply"
-description: Implement tasks from an OpenSpec change (Experimental)
-category: Workflow
-tags: [workflow, artifacts, experimental]
----
-
-Implement tasks from an OpenSpec change.
-
-**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
-
-**Steps**
-
-1. **Select the change**
-
-   If a name is provided, use it. Otherwise:
-   - Infer from conversation context if the user mentioned a change
-   - Auto-select if only one active change exists
-   - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
-
-   Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
-
-2. **Check status to understand the schema**
-   ```bash
-   openspec status --change "<name>" --json
-   ```
-   Parse the JSON to understand:
-   - `schemaName`: The workflow being used (e.g., "spec-driven")
-   - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
-
-3. **Get apply instructions**
-
-   ```bash
-   openspec instructions apply --change "<name>" --json
-   ```
-
-   This returns:
-   - Context file paths (varies by schema)
-   - Progress (total, complete, remaining)
-   - Task list with status
-   - Dynamic instruction based on current state
-
-   **Handle states:**
-   - If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue`
-   - If `state: "all_done"`: congratulate, suggest archive
-   - Otherwise: proceed to implementation
-
-4. **Read context files**
-
-   Read the files listed in `contextFiles` from the apply instructions output.
-   The files depend on the schema being used:
-   - **spec-driven**: proposal, specs, design, tasks
-   - Other schemas: follow the contextFiles from CLI output
-
-5. **Show current progress**
-
-   Display:
-   - Schema being used
-   - Progress: "N/M tasks complete"
-   - Remaining tasks overview
-   - Dynamic instruction from CLI
-
-6. **Implement tasks (loop until done or blocked)**
-
-   For each pending task:
-   - Show which task is being worked on
-   - Make the code changes required
-   - Keep changes minimal and focused
-   - Mark task complete in the tasks file: `- [ ]` → `- [x]`
-   - Continue to next task
-
-   **Pause if:**
-   - Task is unclear → ask for clarification
-   - Implementation reveals a design issue → suggest updating artifacts
-   - Error or blocker encountered → report and wait for guidance
-   - User interrupts
-
-7. **On completion or pause, show status**
-
-   Display:
-   - Tasks completed this session
-   - Overall progress: "N/M tasks complete"
-   - If all done: suggest archive
-   - If paused: explain why and wait for guidance
-
-**Output During Implementation**
-
-```
-## Implementing: <change-name> (schema: <schema-name>)
-
-Working on task 3/7: <task description>
-[...implementation happening...]
-✓ Task complete
-
-Working on task 4/7: <task description>
-[...implementation happening...]
-✓ Task complete
-```
-
-**Output On Completion**
-
-```
-## Implementation Complete
-
-**Change:** <change-name>
-**Schema:** <schema-name>
-**Progress:** 7/7 tasks complete ✓
-
-### Completed This Session
-- [x] Task 1
-- [x] Task 2
-...
-
-All tasks complete! You can archive this change with `/opsx:archive`.
-```
-
-**Output On Pause (Issue Encountered)**
-
-```
-## Implementation Paused
-
-**Change:** <change-name>
-**Schema:** <schema-name>
-**Progress:** 4/7 tasks complete
-
-### Issue Encountered
-<description of the issue>
-
-**Options:**
-1. <option 1>
-2. <option 2>
-3. Other approach
-
-What would you like to do?
-```
-
-**Guardrails**
-- Keep going through tasks until done or blocked
-- Always read context files before starting (from the apply instructions output)
-- If task is ambiguous, pause and ask before implementing
-- If implementation reveals issues, pause and suggest artifact updates
-- Keep code changes minimal and scoped to each task
-- Update task checkbox immediately after completing each task
-- Pause on errors, blockers, or unclear requirements - don't guess
-- Use contextFiles from CLI output, don't assume specific file names
-
-**Fluid Workflow Integration**
-
-This skill supports the "actions on a change" model:
-
-- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
-- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly

+ 0 - 157
.claude/commands/opsx/archive.md

@@ -1,157 +0,0 @@
----
-name: "OPSX: Archive"
-description: Archive a completed change in the experimental workflow
-category: Workflow
-tags: [workflow, archive, experimental]
----
-
-Archive a completed change in the experimental workflow.
-
-**Input**: Optionally specify a change name after `/opsx:archive` (e.g., `/opsx:archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
-
-**Steps**
-
-1. **If no change name provided, prompt for selection**
-
-   Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
-
-   Show only active changes (not already archived).
-   Include the schema used for each change if available.
-
-   **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
-
-2. **Check artifact completion status**
-
-   Run `openspec status --change "<name>" --json` to check artifact completion.
-
-   Parse the JSON to understand:
-   - `schemaName`: The workflow being used
-   - `artifacts`: List of artifacts with their status (`done` or other)
-
-   **If any artifacts are not `done`:**
-   - Display warning listing incomplete artifacts
-   - Prompt user for confirmation to continue
-   - Proceed if user confirms
-
-3. **Check task completion status**
-
-   Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
-
-   Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
-
-   **If incomplete tasks found:**
-   - Display warning showing count of incomplete tasks
-   - Prompt user for confirmation to continue
-   - Proceed if user confirms
-
-   **If no tasks file exists:** Proceed without task-related warning.
-
-4. **Assess delta spec sync state**
-
-   Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
-
-   **If delta specs exist:**
-   - Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
-   - Determine what changes would be applied (adds, modifications, removals, renames)
-   - Show a combined summary before prompting
-
-   **Prompt options:**
-   - If changes needed: "Sync now (recommended)", "Archive without syncing"
-   - If already synced: "Archive now", "Sync anyway", "Cancel"
-
-   If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
-
-5. **Perform the archive**
-
-   Create the archive directory if it doesn't exist:
-   ```bash
-   mkdir -p openspec/changes/archive
-   ```
-
-   Generate target name using current date: `YYYY-MM-DD-<change-name>`
-
-   **Check if target already exists:**
-   - If yes: Fail with error, suggest renaming existing archive or using different date
-   - If no: Move the change directory to archive
-
-   ```bash
-   mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
-   ```
-
-6. **Display summary**
-
-   Show archive completion summary including:
-   - Change name
-   - Schema that was used
-   - Archive location
-   - Spec sync status (synced / sync skipped / no delta specs)
-   - Note about any warnings (incomplete artifacts/tasks)
-
-**Output On Success**
-
-```
-## Archive Complete
-
-**Change:** <change-name>
-**Schema:** <schema-name>
-**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
-**Specs:** ✓ Synced to main specs
-
-All artifacts complete. All tasks complete.
-```
-
-**Output On Success (No Delta Specs)**
-
-```
-## Archive Complete
-
-**Change:** <change-name>
-**Schema:** <schema-name>
-**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
-**Specs:** No delta specs
-
-All artifacts complete. All tasks complete.
-```
-
-**Output On Success With Warnings**
-
-```
-## Archive Complete (with warnings)
-
-**Change:** <change-name>
-**Schema:** <schema-name>
-**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
-**Specs:** Sync skipped (user chose to skip)
-
-**Warnings:**
-- Archived with 2 incomplete artifacts
-- Archived with 3 incomplete tasks
-- Delta spec sync was skipped (user chose to skip)
-
-Review the archive if this was not intentional.
-```
-
-**Output On Error (Archive Exists)**
-
-```
-## Archive Failed
-
-**Change:** <change-name>
-**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
-
-Target archive directory already exists.
-
-**Options:**
-1. Rename the existing archive
-2. Delete the existing archive if it's a duplicate
-3. Wait until a different date to archive
-```
-
-**Guardrails**
-- Always prompt for change selection if not provided
-- Use artifact graph (openspec status --json) for completion checking
-- Don't block archive on warnings - just inform and confirm
-- Preserve .openspec.yaml when moving to archive (it moves with the directory)
-- Show clear summary of what happened
-- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
-- If delta specs exist, always run the sync assessment and show the combined summary before prompting

+ 0 - 173
.claude/commands/opsx/explore.md

@@ -1,173 +0,0 @@
----
-name: "OPSX: Explore"
-description: "Enter explore mode - think through ideas, investigate problems, clarify requirements"
-category: Workflow
-tags: [workflow, explore, experimental, thinking]
----
-
-Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
-
-**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
-
-**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
-
-**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
-- A vague idea: "real-time collaboration"
-- A specific problem: "the auth system is getting unwieldy"
-- A change name: "add-dark-mode" (to explore in context of that change)
-- A comparison: "postgres vs sqlite for this"
-- Nothing (just enter explore mode)
-
----
-
-## The Stance
-
-- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
-- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
-- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
-- **Adaptive** - Follow interesting threads, pivot when new information emerges
-- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
-- **Grounded** - Explore the actual codebase when relevant, don't just theorize
-
----
-
-## What You Might Do
-
-Depending on what the user brings, you might:
-
-**Explore the problem space**
-- Ask clarifying questions that emerge from what they said
-- Challenge assumptions
-- Reframe the problem
-- Find analogies
-
-**Investigate the codebase**
-- Map existing architecture relevant to the discussion
-- Find integration points
-- Identify patterns already in use
-- Surface hidden complexity
-
-**Compare options**
-- Brainstorm multiple approaches
-- Build comparison tables
-- Sketch tradeoffs
-- Recommend a path (if asked)
-
-**Visualize**
-```
-┌─────────────────────────────────────────┐
-│     Use ASCII diagrams liberally        │
-├─────────────────────────────────────────┤
-│                                         │
-│      ┌────────┐         ┌────────┐      │
-│      │ State  │────────▶│ State  │      │
-│      │   A    │         │   B    │      │
-│      └────────┘         └────────┘      │
-│                                         │
-│   System diagrams, state machines,      │
-│   data flows, architecture sketches,    │
-│   dependency graphs, comparison tables  │
-│                                         │
-└─────────────────────────────────────────┘
-```
-
-**Surface risks and unknowns**
-- Identify what could go wrong
-- Find gaps in understanding
-- Suggest spikes or investigations
-
----
-
-## OpenSpec Awareness
-
-You have full context of the OpenSpec system. Use it naturally, don't force it.
-
-### Check for context
-
-At the start, quickly check what exists:
-```bash
-openspec list --json
-```
-
-This tells you:
-- If there are active changes
-- Their names, schemas, and status
-- What the user might be working on
-
-If the user mentioned a specific change name, read its artifacts for context.
-
-### When no change exists
-
-Think freely. When insights crystallize, you might offer:
-
-- "This feels solid enough to start a change. Want me to create a proposal?"
-- Or keep exploring - no pressure to formalize
-
-### When a change exists
-
-If the user mentions a change or you detect one is relevant:
-
-1. **Read existing artifacts for context**
-   - `openspec/changes/<name>/proposal.md`
-   - `openspec/changes/<name>/design.md`
-   - `openspec/changes/<name>/tasks.md`
-   - etc.
-
-2. **Reference them naturally in conversation**
-   - "Your design mentions using Redis, but we just realized SQLite fits better..."
-   - "The proposal scopes this to premium users, but we're now thinking everyone..."
-
-3. **Offer to capture when decisions are made**
-
-    | Insight Type               | Where to Capture               |
-    |----------------------------|--------------------------------|
-    | New requirement discovered | `specs/<capability>/spec.md` |
-    | Requirement changed        | `specs/<capability>/spec.md` |
-    | Design decision made       | `design.md`                  |
-    | Scope changed              | `proposal.md`                |
-    | New work identified        | `tasks.md`                   |
-    | Assumption invalidated     | Relevant artifact              |
-
-   Example offers:
-   - "That's a design decision. Capture it in design.md?"
-   - "This is a new requirement. Add it to specs?"
-   - "This changes scope. Update the proposal?"
-
-4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
-
----
-
-## What You Don't Have To Do
-
-- Follow a script
-- Ask the same questions every time
-- Produce a specific artifact
-- Reach a conclusion
-- Stay on topic if a tangent is valuable
-- Be brief (this is thinking time)
-
----
-
-## Ending Discovery
-
-There's no required ending. Discovery might:
-
-- **Flow into a proposal**: "Ready to start? I can create a change proposal."
-- **Result in artifact updates**: "Updated design.md with these decisions"
-- **Just provide clarity**: User has what they need, moves on
-- **Continue later**: "We can pick this up anytime"
-
-When things crystallize, you might offer a summary - but it's optional. Sometimes the thinking IS the value.
-
----
-
-## Guardrails
-
-- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
-- **Don't fake understanding** - If something is unclear, dig deeper
-- **Don't rush** - Discovery is thinking time, not task time
-- **Don't force structure** - Let patterns emerge naturally
-- **Don't auto-capture** - Offer to save insights, don't just do it
-- **Do visualize** - A good diagram is worth many paragraphs
-- **Do explore the codebase** - Ground discussions in reality
-- **Do question assumptions** - Including the user's and your own

+ 0 - 106
.claude/commands/opsx/propose.md

@@ -1,106 +0,0 @@
----
-name: "OPSX: Propose"
-description: Propose a new change - create it and generate all artifacts in one step
-category: Workflow
-tags: [workflow, artifacts, experimental]
----
-
-Propose a new change - create the change and generate all artifacts in one step.
-
-I'll create a change with artifacts:
-- proposal.md (what & why)
-- design.md (how)
-- tasks.md (implementation steps)
-
-When ready to implement, run /opsx:apply
-
----
-
-**Input**: The argument after `/opsx:propose` is the change name (kebab-case), OR a description of what the user wants to build.
-
-**Steps**
-
-1. **If no input provided, ask what they want to build**
-
-   Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
-   > "What change do you want to work on? Describe what you want to build or fix."
-
-   From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
-
-   **IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
-
-2. **Create the change directory**
-   ```bash
-   openspec new change "<name>"
-   ```
-   This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
-
-3. **Get the artifact build order**
-   ```bash
-   openspec status --change "<name>" --json
-   ```
-   Parse the JSON to get:
-   - `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
-   - `artifacts`: list of all artifacts with their status and dependencies
-
-4. **Create artifacts in sequence until apply-ready**
-
-   Use the **TodoWrite tool** to track progress through the artifacts.
-
-   Loop through artifacts in dependency order (artifacts with no pending dependencies first):
-
-   a. **For each artifact that is `ready` (dependencies satisfied)**:
-      - Get instructions:
-        ```bash
-        openspec instructions <artifact-id> --change "<name>" --json
-        ```
-      - The instructions JSON includes:
-        - `context`: Project background (constraints for you - do NOT include in output)
-        - `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
-        - `template`: The structure to use for your output file
-        - `instruction`: Schema-specific guidance for this artifact type
-        - `outputPath`: Where to write the artifact
-        - `dependencies`: Completed artifacts to read for context
-      - Read any completed dependency files for context
-      - Create the artifact file using `template` as the structure
-      - Apply `context` and `rules` as constraints - but do NOT copy them into the file
-      - Show brief progress: "Created <artifact-id>"
-
-   b. **Continue until all `applyRequires` artifacts are complete**
-      - After creating each artifact, re-run `openspec status --change "<name>" --json`
-      - Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
-      - Stop when all `applyRequires` artifacts are done
-
-   c. **If an artifact requires user input** (unclear context):
-      - Use **AskUserQuestion tool** to clarify
-      - Then continue with creation
-
-5. **Show final status**
-   ```bash
-   openspec status --change "<name>"
-   ```
-
-**Output**
-
-After completing all artifacts, summarize:
-- Change name and location
-- List of artifacts created with brief descriptions
-- What's ready: "All artifacts created! Ready for implementation."
-- Prompt: "Run `/opsx:apply` to start implementing."
-
-**Artifact Creation Guidelines**
-
-- Follow the `instruction` field from `openspec instructions` for each artifact type
-- The schema defines what each artifact should contain - follow it
-- Read dependency artifacts for context before creating new ones
-- Use `template` as the structure for your output file - fill in its sections
-- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
-  - Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
-  - These guide what you write, but should never appear in the output
-
-**Guardrails**
-- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
-- Always read dependency artifacts before creating a new one
-- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
-- If a change with that name already exists, ask if user wants to continue it or create a new one
-- Verify each artifact file exists after writing before proceeding to next

+ 0 - 156
.claude/skills/openspec-apply-change/SKILL.md

@@ -1,156 +0,0 @@
----
-name: openspec-apply-change
-description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
-license: MIT
-compatibility: Requires openspec CLI.
-metadata:
-  author: openspec
-  version: "1.0"
-  generatedBy: "1.3.0"
----
-
-Implement tasks from an OpenSpec change.
-
-**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
-
-**Steps**
-
-1. **Select the change**
-
-   If a name is provided, use it. Otherwise:
-   - Infer from conversation context if the user mentioned a change
-   - Auto-select if only one active change exists
-   - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
-
-   Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
-
-2. **Check status to understand the schema**
-   ```bash
-   openspec status --change "<name>" --json
-   ```
-   Parse the JSON to understand:
-   - `schemaName`: The workflow being used (e.g., "spec-driven")
-   - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
-
-3. **Get apply instructions**
-
-   ```bash
-   openspec instructions apply --change "<name>" --json
-   ```
-
-   This returns:
-   - Context file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
-   - Progress (total, complete, remaining)
-   - Task list with status
-   - Dynamic instruction based on current state
-
-   **Handle states:**
-   - If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
-   - If `state: "all_done"`: congratulate, suggest archive
-   - Otherwise: proceed to implementation
-
-4. **Read context files**
-
-   Read the files listed in `contextFiles` from the apply instructions output.
-   The files depend on the schema being used:
-   - **spec-driven**: proposal, specs, design, tasks
-   - Other schemas: follow the contextFiles from CLI output
-
-5. **Show current progress**
-
-   Display:
-   - Schema being used
-   - Progress: "N/M tasks complete"
-   - Remaining tasks overview
-   - Dynamic instruction from CLI
-
-6. **Implement tasks (loop until done or blocked)**
-
-   For each pending task:
-   - Show which task is being worked on
-   - Make the code changes required
-   - Keep changes minimal and focused
-   - Mark task complete in the tasks file: `- [ ]` → `- [x]`
-   - Continue to next task
-
-   **Pause if:**
-   - Task is unclear → ask for clarification
-   - Implementation reveals a design issue → suggest updating artifacts
-   - Error or blocker encountered → report and wait for guidance
-   - User interrupts
-
-7. **On completion or pause, show status**
-
-   Display:
-   - Tasks completed this session
-   - Overall progress: "N/M tasks complete"
-   - If all done: suggest archive
-   - If paused: explain why and wait for guidance
-
-**Output During Implementation**
-
-```
-## Implementing: <change-name> (schema: <schema-name>)
-
-Working on task 3/7: <task description>
-[...implementation happening...]
-✓ Task complete
-
-Working on task 4/7: <task description>
-[...implementation happening...]
-✓ Task complete
-```
-
-**Output On Completion**
-
-```
-## Implementation Complete
-
-**Change:** <change-name>
-**Schema:** <schema-name>
-**Progress:** 7/7 tasks complete ✓
-
-### Completed This Session
-- [x] Task 1
-- [x] Task 2
-...
-
-All tasks complete! Ready to archive this change.
-```
-
-**Output On Pause (Issue Encountered)**
-
-```
-## Implementation Paused
-
-**Change:** <change-name>
-**Schema:** <schema-name>
-**Progress:** 4/7 tasks complete
-
-### Issue Encountered
-<description of the issue>
-
-**Options:**
-1. <option 1>
-2. <option 2>
-3. Other approach
-
-What would you like to do?
-```
-
-**Guardrails**
-- Keep going through tasks until done or blocked
-- Always read context files before starting (from the apply instructions output)
-- If task is ambiguous, pause and ask before implementing
-- If implementation reveals issues, pause and suggest artifact updates
-- Keep code changes minimal and scoped to each task
-- Update task checkbox immediately after completing each task
-- Pause on errors, blockers, or unclear requirements - don't guess
-- Use contextFiles from CLI output, don't assume specific file names
-
-**Fluid Workflow Integration**
-
-This skill supports the "actions on a change" model:
-
-- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
-- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly

+ 0 - 114
.claude/skills/openspec-archive-change/SKILL.md

@@ -1,114 +0,0 @@
----
-name: openspec-archive-change
-description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
-license: MIT
-compatibility: Requires openspec CLI.
-metadata:
-  author: openspec
-  version: "1.0"
-  generatedBy: "1.3.0"
----
-
-Archive a completed change in the experimental workflow.
-
-**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
-
-**Steps**
-
-1. **If no change name provided, prompt for selection**
-
-   Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
-
-   Show only active changes (not already archived).
-   Include the schema used for each change if available.
-
-   **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
-
-2. **Check artifact completion status**
-
-   Run `openspec status --change "<name>" --json` to check artifact completion.
-
-   Parse the JSON to understand:
-   - `schemaName`: The workflow being used
-   - `artifacts`: List of artifacts with their status (`done` or other)
-
-   **If any artifacts are not `done`:**
-   - Display warning listing incomplete artifacts
-   - Use **AskUserQuestion tool** to confirm user wants to proceed
-   - Proceed if user confirms
-
-3. **Check task completion status**
-
-   Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
-
-   Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
-
-   **If incomplete tasks found:**
-   - Display warning showing count of incomplete tasks
-   - Use **AskUserQuestion tool** to confirm user wants to proceed
-   - Proceed if user confirms
-
-   **If no tasks file exists:** Proceed without task-related warning.
-
-4. **Assess delta spec sync state**
-
-   Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
-
-   **If delta specs exist:**
-   - Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
-   - Determine what changes would be applied (adds, modifications, removals, renames)
-   - Show a combined summary before prompting
-
-   **Prompt options:**
-   - If changes needed: "Sync now (recommended)", "Archive without syncing"
-   - If already synced: "Archive now", "Sync anyway", "Cancel"
-
-   If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
-
-5. **Perform the archive**
-
-   Create the archive directory if it doesn't exist:
-   ```bash
-   mkdir -p openspec/changes/archive
-   ```
-
-   Generate target name using current date: `YYYY-MM-DD-<change-name>`
-
-   **Check if target already exists:**
-   - If yes: Fail with error, suggest renaming existing archive or using different date
-   - If no: Move the change directory to archive
-
-   ```bash
-   mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
-   ```
-
-6. **Display summary**
-
-   Show archive completion summary including:
-   - Change name
-   - Schema that was used
-   - Archive location
-   - Whether specs were synced (if applicable)
-   - Note about any warnings (incomplete artifacts/tasks)
-
-**Output On Success**
-
-```
-## Archive Complete
-
-**Change:** <change-name>
-**Schema:** <schema-name>
-**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
-**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
-
-All artifacts complete. All tasks complete.
-```
-
-**Guardrails**
-- Always prompt for change selection if not provided
-- Use artifact graph (openspec status --json) for completion checking
-- Don't block archive on warnings - just inform and confirm
-- Preserve .openspec.yaml when moving to archive (it moves with the directory)
-- Show clear summary of what happened
-- If sync is requested, use openspec-sync-specs approach (agent-driven)
-- If delta specs exist, always run the sync assessment and show the combined summary before prompting

+ 0 - 288
.claude/skills/openspec-explore/SKILL.md

@@ -1,288 +0,0 @@
----
-name: openspec-explore
-description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
-license: MIT
-compatibility: Requires openspec CLI.
-metadata:
-  author: openspec
-  version: "1.0"
-  generatedBy: "1.3.0"
----
-
-Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
-
-**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
-
-**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
-
----
-
-## The Stance
-
-- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
-- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
-- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
-- **Adaptive** - Follow interesting threads, pivot when new information emerges
-- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
-- **Grounded** - Explore the actual codebase when relevant, don't just theorize
-
----
-
-## What You Might Do
-
-Depending on what the user brings, you might:
-
-**Explore the problem space**
-- Ask clarifying questions that emerge from what they said
-- Challenge assumptions
-- Reframe the problem
-- Find analogies
-
-**Investigate the codebase**
-- Map existing architecture relevant to the discussion
-- Find integration points
-- Identify patterns already in use
-- Surface hidden complexity
-
-**Compare options**
-- Brainstorm multiple approaches
-- Build comparison tables
-- Sketch tradeoffs
-- Recommend a path (if asked)
-
-**Visualize**
-```
-┌─────────────────────────────────────────┐
-│     Use ASCII diagrams liberally        │
-├─────────────────────────────────────────┤
-│                                         │
-│      ┌────────┐         ┌────────┐      │
-│      │ State  │────────▶│ State  │      │
-│      │   A    │         │   B    │      │
-│      └────────┘         └────────┘      │
-│                                         │
-│   System diagrams, state machines,      │
-│   data flows, architecture sketches,    │
-│   dependency graphs, comparison tables  │
-│                                         │
-└─────────────────────────────────────────┘
-```
-
-**Surface risks and unknowns**
-- Identify what could go wrong
-- Find gaps in understanding
-- Suggest spikes or investigations
-
----
-
-## OpenSpec Awareness
-
-You have full context of the OpenSpec system. Use it naturally, don't force it.
-
-### Check for context
-
-At the start, quickly check what exists:
-```bash
-openspec list --json
-```
-
-This tells you:
-- If there are active changes
-- Their names, schemas, and status
-- What the user might be working on
-
-### When no change exists
-
-Think freely. When insights crystallize, you might offer:
-
-- "This feels solid enough to start a change. Want me to create a proposal?"
-- Or keep exploring - no pressure to formalize
-
-### When a change exists
-
-If the user mentions a change or you detect one is relevant:
-
-1. **Read existing artifacts for context**
-   - `openspec/changes/<name>/proposal.md`
-   - `openspec/changes/<name>/design.md`
-   - `openspec/changes/<name>/tasks.md`
-   - etc.
-
-2. **Reference them naturally in conversation**
-   - "Your design mentions using Redis, but we just realized SQLite fits better..."
-   - "The proposal scopes this to premium users, but we're now thinking everyone..."
-
-3. **Offer to capture when decisions are made**
-
-    | Insight Type               | Where to Capture               |
-    |----------------------------|--------------------------------|
-    | New requirement discovered | `specs/<capability>/spec.md` |
-    | Requirement changed        | `specs/<capability>/spec.md` |
-    | Design decision made       | `design.md`                  |
-    | Scope changed              | `proposal.md`                |
-    | New work identified        | `tasks.md`                   |
-    | Assumption invalidated     | Relevant artifact              |
-
-   Example offers:
-   - "That's a design decision. Capture it in design.md?"
-   - "This is a new requirement. Add it to specs?"
-   - "This changes scope. Update the proposal?"
-
-4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
-
----
-
-## What You Don't Have To Do
-
-- Follow a script
-- Ask the same questions every time
-- Produce a specific artifact
-- Reach a conclusion
-- Stay on topic if a tangent is valuable
-- Be brief (this is thinking time)
-
----
-
-## Handling Different Entry Points
-
-**User brings a vague idea:**
-```
-User: I'm thinking about adding real-time collaboration
-
-You: Real-time collab is a big space. Let me think about this...
-
-      COLLABORATION SPECTRUM
-      ════════════════════════════════════════════
-
-      Awareness          Coordination         Sync
-          │                   │                 │
-          ▼                   ▼                 ▼
-      ┌────────┐         ┌────────┐        ┌────────┐
-      │Presence│         │Cursors │        │  CRDT  │
-      │  "3    │         │ Multi  │        │Conflict│
-      │online" │         │ select │        │  free  │
-      └────────┘         └────────┘        └────────┘
-          │                   │                 │
-       trivial            moderate           complex
-
-      Where's your head at?
-```
-
-**User brings a specific problem:**
-```
-User: The auth system is a mess
-
-You: [reads codebase]
-
-     ┌─────────────────────────────────────────────┐
-     │              CURRENT AUTH FLOW              │
-     └─────────────────────────────────────────────┘
-                        │
-          ┌─────────────┼─────────────┐
-          ▼             ▼             ▼
-     ┌─────────┐  ┌─────────┐  ┌─────────┐
-     │ Google  │  │  GitHub │  │  Email  │
-     │  OAuth  │  │  OAuth  │  │  Magic  │
-     └────┬────┘  └────┬────┘  └────┬────┘
-          │             │             │
-          └─────────────┼─────────────┘
-                        ▼
-                  ┌───────────┐
-                  │  Session  │
-                  └─────┬─────┘
-                        │
-                        ▼
-                  ┌───────────┐
-                  │   Perms   │
-                  └───────────┘
-
-     I see three tangles. Which one's burning?
-```
-
-**User is stuck mid-implementation:**
-```
-User: /opsx:explore add-auth-system
-      The OAuth integration is more complex than expected
-
-You: [reads change artifacts]
-
-     You're on task 4: "Implement OAuth flow"
-
-     Let me trace what's involved...
-
-     [draws diagram, explores options, suggests paths]
-
-     Want to update the design to reflect this?
-     Or add a spike task to investigate?
-```
-
-**User wants to compare options:**
-```
-User: Should we use Postgres or SQLite?
-
-You: Generic answer is boring. What's the context?
-
-User: A CLI tool that tracks local dev environments
-
-You: That changes everything.
-
-     ┌─────────────────────────────────────────────────┐
-     │          CLI TOOL DATA STORAGE                  │
-     └─────────────────────────────────────────────────┘
-
-     Key constraints:
-     • No daemon running
-     • Must work offline
-     • Single user
-
-                  SQLite          Postgres
-     Deployment   embedded ✓      needs server ✗
-     Offline      yes ✓           no ✗
-     Single file  yes ✓           no ✗
-
-     SQLite. Not even close.
-
-     Unless... is there a sync component?
-```
-
----
-
-## Ending Discovery
-
-There's no required ending. Discovery might:
-
-- **Flow into a proposal**: "Ready to start? I can create a change proposal."
-- **Result in artifact updates**: "Updated design.md with these decisions"
-- **Just provide clarity**: User has what they need, moves on
-- **Continue later**: "We can pick this up anytime"
-
-When it feels like things are crystallizing, you might summarize:
-
-```
-## What We Figured Out
-
-**The problem**: [crystallized understanding]
-
-**The approach**: [if one emerged]
-
-**Open questions**: [if any remain]
-
-**Next steps** (if ready):
-- Create a change proposal
-- Keep exploring: just keep talking
-```
-
-But this summary is optional. Sometimes the thinking IS the value.
-
----
-
-## Guardrails
-
-- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
-- **Don't fake understanding** - If something is unclear, dig deeper
-- **Don't rush** - Discovery is thinking time, not task time
-- **Don't force structure** - Let patterns emerge naturally
-- **Don't auto-capture** - Offer to save insights, don't just do it
-- **Do visualize** - A good diagram is worth many paragraphs
-- **Do explore the codebase** - Ground discussions in reality
-- **Do question assumptions** - Including the user's and your own

+ 0 - 110
.claude/skills/openspec-propose/SKILL.md

@@ -1,110 +0,0 @@
----
-name: openspec-propose
-description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
-license: MIT
-compatibility: Requires openspec CLI.
-metadata:
-  author: openspec
-  version: "1.0"
-  generatedBy: "1.3.0"
----
-
-Propose a new change - create the change and generate all artifacts in one step.
-
-I'll create a change with artifacts:
-- proposal.md (what & why)
-- design.md (how)
-- tasks.md (implementation steps)
-
-When ready to implement, run /opsx:apply
-
----
-
-**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
-
-**Steps**
-
-1. **If no clear input provided, ask what they want to build**
-
-   Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
-   > "What change do you want to work on? Describe what you want to build or fix."
-
-   From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
-
-   **IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
-
-2. **Create the change directory**
-   ```bash
-   openspec new change "<name>"
-   ```
-   This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
-
-3. **Get the artifact build order**
-   ```bash
-   openspec status --change "<name>" --json
-   ```
-   Parse the JSON to get:
-   - `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
-   - `artifacts`: list of all artifacts with their status and dependencies
-
-4. **Create artifacts in sequence until apply-ready**
-
-   Use the **TodoWrite tool** to track progress through the artifacts.
-
-   Loop through artifacts in dependency order (artifacts with no pending dependencies first):
-
-   a. **For each artifact that is `ready` (dependencies satisfied)**:
-      - Get instructions:
-        ```bash
-        openspec instructions <artifact-id> --change "<name>" --json
-        ```
-      - The instructions JSON includes:
-        - `context`: Project background (constraints for you - do NOT include in output)
-        - `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
-        - `template`: The structure to use for your output file
-        - `instruction`: Schema-specific guidance for this artifact type
-        - `outputPath`: Where to write the artifact
-        - `dependencies`: Completed artifacts to read for context
-      - Read any completed dependency files for context
-      - Create the artifact file using `template` as the structure
-      - Apply `context` and `rules` as constraints - but do NOT copy them into the file
-      - Show brief progress: "Created <artifact-id>"
-
-   b. **Continue until all `applyRequires` artifacts are complete**
-      - After creating each artifact, re-run `openspec status --change "<name>" --json`
-      - Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
-      - Stop when all `applyRequires` artifacts are done
-
-   c. **If an artifact requires user input** (unclear context):
-      - Use **AskUserQuestion tool** to clarify
-      - Then continue with creation
-
-5. **Show final status**
-   ```bash
-   openspec status --change "<name>"
-   ```
-
-**Output**
-
-After completing all artifacts, summarize:
-- Change name and location
-- List of artifacts created with brief descriptions
-- What's ready: "All artifacts created! Ready for implementation."
-- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
-
-**Artifact Creation Guidelines**
-
-- Follow the `instruction` field from `openspec instructions` for each artifact type
-- The schema defines what each artifact should contain - follow it
-- Read dependency artifacts for context before creating new ones
-- Use `template` as the structure for your output file - fill in its sections
-- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
-  - Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
-  - These guide what you write, but should never appear in the output
-
-**Guardrails**
-- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
-- Always read dependency artifacts before creating a new one
-- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
-- If a change with that name already exists, ask if user wants to continue it or create a new one
-- Verify each artifact file exists after writing before proceeding to next

+ 13 - 48
CLAUDE.md

@@ -1,52 +1,17 @@
-# mjava-ai
+# mjava-ai(程序仓)
 
 
-Java 后端基座 + 客户子项目仓库。Spring Boot 2.2.13 + MySQL,第一阶段不引入 Redis/Docker
+本仓库仅保留 Java 程序代码(pom + src + yml.example)
 
 
-## 开发前必读
+**所有开发设计规范 / OpenSpec changes & specs / 子项目说明 / opsx commands & skills 已迁出到文档中心**:
 
 
-- 仓库基线:`openspec/specs/project-baseline.md`(代码锚点 + 子项目清单)
-- 通用规范:`/Users/malk/Desktop/Tech/claude/后端/CLAUDE.md`(含 Client/Service 分层规则 R1~R7)
-- 宜搭特化:`/Users/malk/Desktop/Tech/claude/后端/.claude/docs/yida-serverside.md`
-- 分层规则 capability spec:`openspec/specs/client-service-layering/spec.md`(archive 后;当前在 `openspec/changes/standardize-client-service-layering/specs/`)
+```
+/Users/malk/Desktop/Tech/claude/后端/mjava-ai/
+├── CLAUDE.md            ← AI 协作主入口(含 opsx 工作流、change 状态、严禁清单)
+├── README.md            ← 完整项目说明
+├── openspec/            ← changes / specs / archive / BACKLOG
+├── .claude/             ← opsx commands + openspec skills
+├── mjava-pro/README.md
+└── mjava-com/README.md
+```
 
 
-## OpenSpec 工作流(opsx)
-
-任何规范变更或功能新增,走 change 流程:
-
-| 命令 | 用途 |
-|------|------|
-| `/opsx:explore` | 在改动前做需求/约束梳理 |
-| `/opsx:propose` | 新建 change(proposal+design+spec+tasks 四件套) |
-| `/opsx:apply` | 执行 tasks.md 的实现步骤 |
-| `/opsx:archive` | 完成后归档到 `openspec/changes/archive/` |
-
-现有 change 状态(2026-04-19 更新):
-
-已归档(`changes/archive/`):
-- `2026-04-18-extract-dingtalk-standard-api` → `specs/crypto-utils/`
-- `2026-04-19-init-project-baseline` → `specs/project-baseline.md`
-- `2026-04-19-extend-yida-api-coverage` → `specs/yida-form-atomic/` + `specs/yida-process-atomic/`
-- `2026-04-19-extend-dingtalk-contacts-api` → `specs/dingtalk-contacts-v2/`
-- `2026-04-19-add-request-auth-replay-guard` → `specs/request-auth/` + `specs/replay-guard/`
-
-进行中(`changes/`):
-- `add-observability-foundation` 12/14 — 待生产冒烟
-- `add-mjava-pro` 15/30 — 骨架完成,TenantTaskDecorator / DynamicDDService 延后
-- `add-mjava-com` 19/30 — 骨架完成,DingtalkActionRegistry / AliworkActionRegistry 首批 action 待补
-
-## 快速操作
-
-- 新客户接入:复制 `mjava-mcli` 模板,详见基线文档第 9 章
-- 本地编译:`mvn -pl mjava-{module} -am clean compile`
-- 打包:`mvn -pl mjava-{module} -am clean package -Dmaven.test.skip=true`
-
-## 严禁
-
-- ❌ 引入三方 SDK(钉钉/宜搭/飞书 SDK 都禁)—— 统一用 `UtilHttp`
-- ❌ 每请求重新拉 token —— 统一走 `UtilToken` 缓存
-- ❌ 硬编码 appKey/appSecret —— 读 `application-{profile}.yml`
-- ❌ 绕过 JPA 写原生 SQL 字符串拼接
-
-## 当前活跃目录位置
-
-`/Users/malk/server/cur/mjava-ai/`(2026-04-18 从 `/Users/malk/server/mjava-ai` 迁入 cur 层)
+后端通用规范见 `/Users/malk/Desktop/Tech/claude/后端/CLAUDE.md`(含 Client/Service 分层规则 R1~R7)。

+ 17 - 179
README.md

@@ -1,190 +1,28 @@
 # mjava-ai
 # mjava-ai
 
 
-> Java 后端基座 + 多客户子项目聚合仓库
-> 技术栈:Spring Boot 2.2.13 + MySQL + JPA + QueryDSL + Hutool
+Java 后端基座 + 客户子项目仓库。Spring Boot 2.2.13 + MySQL。
 
 
-## 定位
+## 模块
 
 
-**mjava** 是一套可复用的 Java 后端基座,封装企业办公常见第三方平台(钉钉 / 宜搭 / 飞书 / 北森 等)的统一调用、鉴权、审计、持久化能力。每个客户需求作为独立子项目接入,引用基座 jar,只关注业务逻辑。
+| 模块 | 用途 |
+|---|---|
+| `mjava/` | 基座 jar(被所有子项目依赖;不可独立运行) |
+| `mjava-mcli/` | 客户模板(端口 9001) |
+| `mjava-pro/` | 多租户单部署运行时(A 档公共托管,端口 9010) |
+| `mjava-com/` | BaaS 网关(对外暴露 mjava 能力,端口 9020) |
 
 
-**第一阶段约束**:仅 Spring Boot + MySQL,不引入 Redis / Docker / K8s / 消息队列。
-
-## 仓库结构
-
-```
-mjava-ai/
-├── mjava/                基座 jar(被所有子项目依赖)
-├── mjava-mcli/           客户模板 + 最小启动实例
-├── mjava-pro/            专项:多客户单部署(宜搭应用表驱动)
-├── mjava-com/            专项:通用能力 BaaS 网关
-├── openspec/             规范驱动开发(changes / specs / archive)
-├── CLAUDE.md             AI 协作入口(本仓库 opsx 工作流指引)
-└── pom.xml               聚合 pom
-```
-
-## 基座能力(`mjava/`)
-
-所有子项目通过 `@SpringBootApplication(scanBasePackages = {"com.malk"})` 自动扫到。
-
-### 核心工具 `com.malk.utils`
-
-| 类 | 能力 |
-|----|------|
-| `UtilHttp` | HTTP 请求统一入口(Hutool 封装,GET/POST/PUT/DELETE/PATCH/UPLOAD/DOWNLOAD/SOAP) |
-| `UtilToken` | access_token 内存缓存(TimedCache,自动冗余 -5s) |
-| `UtilSignature` | HMAC-SHA256 签名 + SHA256 + 常量时间比较 |
-| `UtilMap` / `UtilList` / `UtilString` | 集合/字符串常用辅助 |
-| `UtilDateTime` / `UtilNumber` / `UtilMath` | 时间/数值处理 |
-| `UtilExcel` | EasyExcel 封装 |
-| `UtilFile` / `UtilServlet` / `UtilConvert` | IO / 类型转换 |
-| `UtilEnv` / `UtilMc` / `UtilImport` | 环境 / 杂项 |
-
-### 响应与异常 `com.malk.server.common`
-
-| 类 | 能力 |
-|----|------|
-| `McR<T>` | 统一 Controller 响应(success / code / message / data / source) |
-| `VenR` | 第三方原始响应基类(`assertSuccess()`) |
-| `McException` | 业务异常(`(code, message)` 构造) |
-| `McREnum` | 错误码枚举 |
-| `McPage` | 分页封装 |
-
-### 过滤与拦截 `com.malk.filter`
-
-| 类 | 能力 |
-|----|------|
-| `TraceIdFilter` | 请求 traceId 注入 MDC,X-Trace-Id 响应头回传 |
-| `CatchException` | 全局异常处理(`@RestControllerAdvice`) |
-| `RequestInterceptor` | 请求日志(point logger) |
-| `AuthFilter` + `AuthInterceptor` | 请求鉴权 + 防重放(HMAC + 时间窗 + Nonce),默认关闭 |
-| `NoAuth` | 鉴权豁免注解 |
-
-### 持久化 `com.malk.base`
-
-| 类 | 能力 |
-|----|------|
-| `BasePo` | 实体基类(自动审计 createTime/updateTime/createBy/updateBy) |
-| `BaseDto` | DTO 基类 |
-| `BaseRepository<PO, ID>` | JPA Repository + QueryDSL 基类 |
-| `BaseDao` / `JpaMap` | DAO 辅助 |
-
-### 配置 `com.malk.config`
-
-| 类 | 能力 |
-|----|------|
-| `WebConfiguration` | MVC 拦截器注册 + CORS |
-| `DataSourceConfig` | 多数据源(`spel.multiSource=true` 开启) |
-| `JpaConfiguration` | JPA 审计 |
-| `OpenApiConfig` | Swagger UI(`swagger.enable=true` 开启) |
-| `AsyncConfig` | 线程池 + `MdcTaskDecorator` |
-| `AuthConfigProperties` | 基座鉴权配置 |
-
-### 第三方对接 `com.malk.service.{vendor}`
-
-| Vendor | 已覆盖 Domain |
-|--------|---------------|
-| **dingtalk** | Contacts(v2 对齐 27 方法)/ Attendance / Workflow / Notice / Storage / Group / Schedule / Event / Personnel / Report / Extension / Dedicated |
-| **aliwork**(宜搭) | Form(原子 16 方法)/ Process(原子 13 方法)/ 旧聚合 YDClient(保留) |
-| **beisen**(北森) | Employee / Attendance |
-| **teambition** | Client / Service |
-| **fxiaoke**(纷享销客) | Client |
-| **h3yun**(氚云) | Client |
-| **vika**(维格表) | Client |
-| **xbongbong** | Client |
-| **ekuaibao**(易快报) | Client |
-
-每个 vendor 遵循 **Client**(原子)+ **Service**(组合)两层分层。
-
-### 其他模块
-
-| 包 | 能力 |
-|----|------|
-| `core/AsyncConfig` + `MdcTaskDecorator` | @Async 线程池 + MDC 传递 |
-| `core/NonceCache` | LRU+TTL Nonce 去重 |
-| `delegate/` | DDEvent / TBEvent 委托回调 |
-| `schedule/McScheduleTask` | `@Scheduled` 定时任务基类 |
-| `repository/dao+entity/{primary,slave}/` | 多数据源示例 |
-
-## 子项目速览
-
-| 子模块 | 端口 | Context | 状态 |
-|--------|------|---------|------|
-| `mjava-mcli` | 9001 | /api/mcli | 新客户模板 |
-| `mjava-pro` | 9010 | /api/pro | 专项骨架(多客户单部署,宜搭应用表驱动租户配置) |
-| `mjava-com` | 9020 | /api/com | 专项骨架(BaaS 网关,HMAC 签名 + 限流) |
-
-## 核心约定
-
-- Controller 返回 `McR<T>`;业务异常 `throw new McException(code, message)`
-- HTTP **禁三方 SDK**,统一 `UtilHttp` / `DDR.doPost` / `DDR_New.doPost`
-- Token **禁每请求重取**,走 `UtilToken`(key `{vendor}:{appKey}`)
-- 鉴权参数从 `application-{profile}.yml` 读,**禁硬编码**
-- JPA + QueryDSL 持久化,**不与 MyBatis 混用**
-- 生产配置 `application-prod.yml` 不入 git,`*.yml.example` 入 git
-
-详细规范见**文档中心** `/Users/malk/Desktop/Tech/claude/后端/CLAUDE.md`。
-
-## 快速开始
-
-```bash
-# 编译
-mvn -pl mjava-mcli -am clean compile
-
-# 打包
-mvn -pl mjava-mcli -am clean package -DskipTests
-
-# 启动
-java -jar mjava-mcli/target/mjava-mcli.jar --spring.profiles.active=prod
-```
-
-## 新客户接入
+## 构建
 
 
 ```bash
 ```bash
-cp -r mjava-mcli mjava-{customer}
-
-# 1. pom.xml: artifactId=mjava-{customer}
-# 2. Boot.java: package=com.malk.{customer}, 保留 scanBasePackages={"com.malk"}
-# 3. application-*.yml: server.port / context-path / datasource / vendor 凭据
-# 4. 根 pom.xml <modules> 追加
-
-mvn -pl mjava-{customer} -am clean compile
+mvn -pl mjava-{module} -am clean compile
+mvn -pl mjava-{module} -am clean package -Dmaven.test.skip=true
+java -jar mjava-{module}/target/mjava-{module}.jar --spring.profiles.active=prod
 ```
 ```
 
 
-详见文档中心 `/Users/malk/Desktop/Tech/claude/后端/CLAUDE.md` → "子项目接入流程"。
-
-## OpenSpec 扩展
-
-本仓库内 `openspec/` 驱动演进:
-
-```bash
-/opsx:propose   # 新 change 提案
-/opsx:apply     # 实施 tasks.md
-/opsx:archive   # 归档完成的 change + 自动合并稳态 spec
-/opsx:explore   # 需求/约束梳理
-```
-
-已归档稳态 capability(位于 `openspec/specs/`):
-
-| capability | 覆盖 |
-|-----------|------|
-| `project-baseline` | 仓库级基线指针 |
-| `crypto-utils` | RSA 加密工具 |
-| `yida-form-atomic` | 宜搭表单原子接口(16 方法) |
-| `yida-process-atomic` | 宜搭流程原子接口(13 方法) |
-| `dingtalk-contacts-v2` | 钉钉通讯录 v2 对齐(27 方法) |
-| `request-auth` + `replay-guard` | 基座 HMAC 鉴权 + 防重放 |
-
-## 文档索引
-
-| 文档 | 用途 |
-|------|------|
-| `./CLAUDE.md` | 本仓库 AI 协作入口(opsx 工作流指引 + change 状态) |
-| `/Users/malk/Desktop/Tech/claude/后端/CLAUDE.md` | **后端开发主规范**(必读) |
-| `/Users/malk/Desktop/Tech/claude/后端/.claude/docs/yida-serverside.md` | 宜搭 YDClient Java 特化规范 |
-| `/Users/malk/Desktop/Tech/claude/.claude/docs/data-structures.md` | 宜搭字段数据结构(前后端通用) |
-| `/Users/malk/Desktop/Tech/claude/.claude/docs/search-field-format.md` | 宜搭查询条件格式 |
-| `/Users/malk/Desktop/Tech/claude/宜搭/CLAUDE.md` | 宜搭前端 JS/JSX 规范 |
+## 文档与设计规范
 
 
-## 远端
+> **本仓库仅保留程序代码**。完整 README / CLAUDE / OpenSpec changes & specs / 子模块说明等开发设计规范统一在文档中心管理:
+>
+> `/Users/malk/Desktop/Tech/claude/后端/mjava-ai/`
 
 
-`https://mc.cloudpure.cn/vibeCoding/mjava-ai.git`
+后端开发通用规范见 `/Users/malk/Desktop/Tech/claude/后端/CLAUDE.md`。

+ 94 - 0
mjava-pro/src/main/java/com/malk/pro/service/aliwork/DynamicYDService.java

@@ -0,0 +1,94 @@
+package com.malk.pro.service.aliwork;
+
+import com.malk.pro.service.dingtalk.DynamicDDService;
+import com.malk.pro.tenant.TenantContext;
+import com.malk.pro.tenant.TenantProfile;
+import com.malk.pro.tenant.VendorCredential;
+import com.malk.server.aliwork.YDAuth;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.common.McException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * 多租户宜搭服务(mjava-pro 容器)
+ *
+ * <p>capability: multi-tenant-runtime。从 {@link TenantContext} 取当前租户的宜搭凭据,
+ * 构造 {@link YDAuth} 供 {@code YDClient_Form} / {@code YDClient_Process} 原子接口使用。</p>
+ *
+ * <p>调用方范式:</p>
+ * <pre>
+ * &#64;Autowired DynamicYDService ydService;
+ * &#64;Autowired YDClient_Form ydForm;
+ * ...
+ * ydForm.searchForm(ydService.buildAuth(), formUuid, ...);
+ * </pre>
+ *
+ * <p>宜搭凭据约定:{@code VendorCredential.extra} 必填 {@code appType} / {@code systemToken},
+ * 可选 {@code userId}(缺省走 {@link YDConf#PUB_ACCOUNT})。</p>
+ */
+@Slf4j
+@Service
+public class DynamicYDService {
+
+    private static final String VENDOR = "aliwork";
+
+    @Autowired
+    private DynamicDDService ddService;
+
+    /**
+     * 当前租户宜搭凭据。租户/凭据缺失抛 {@link McException}。
+     */
+    public VendorCredential currentCredential() {
+        TenantProfile profile = TenantContext.current();
+        if (profile == null) {
+            throw new McException("TENANT_REQUIRED", "TenantContext 未注入,无法获取宜搭凭据");
+        }
+        VendorCredential credential = profile.credential(VENDOR);
+        if (credential == null) {
+            throw new McException("VENDOR_NOT_CONFIGURED",
+                    "tenant=" + profile.getTenantId() + " 未配置 aliwork 凭据");
+        }
+        return credential;
+    }
+
+    /**
+     * 构造 {@link YDAuth}:accessToken 取自钉钉租户,appType/systemToken/userId 取自宜搭 extra。
+     */
+    public YDAuth buildAuth() {
+        return buildAuth(null);
+    }
+
+    /**
+     * 构造 {@link YDAuth} 并指定操作人 userId(覆盖 extra 中的默认 userId)。
+     */
+    public YDAuth buildAuth(String userIdOverride) {
+        VendorCredential credential = currentCredential();
+        Map<String, Object> extra = Optional.ofNullable(credential.getExtra()).orElseThrow(() ->
+                new McException("YDA_CONFIG_INCOMPLETE",
+                        "tenant=" + TenantContext.currentTenantId() + " aliwork.extra 缺失 appType/systemToken"));
+        String appType = stringValue(extra, "appType");
+        String systemToken = stringValue(extra, "systemToken");
+        if (appType == null || systemToken == null) {
+            throw new McException("YDA_CONFIG_INCOMPLETE",
+                    "tenant=" + TenantContext.currentTenantId() + " 宜搭 extra 缺少 appType/systemToken");
+        }
+        String userId = userIdOverride != null ? userIdOverride
+                : Optional.ofNullable(stringValue(extra, "userId")).orElse(YDConf.PUB_ACCOUNT);
+        return YDAuth.builder()
+                .accessToken(ddService.getAccessToken())
+                .appType(appType)
+                .systemToken(systemToken)
+                .userId(userId)
+                .build();
+    }
+
+    private static String stringValue(Map<String, Object> map, String key) {
+        Object v = map.get(key);
+        return v == null ? null : v.toString();
+    }
+}

+ 82 - 0
mjava-pro/src/main/java/com/malk/pro/service/dingtalk/DynamicDDService.java

@@ -0,0 +1,82 @@
+package com.malk.pro.service.dingtalk;
+
+import com.malk.pro.tenant.TenantContext;
+import com.malk.pro.tenant.TenantProfile;
+import com.malk.pro.tenant.VendorCredential;
+import com.malk.server.common.McException;
+import com.malk.service.dingtalk.DDClient;
+import com.malk.utils.UtilToken;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 多租户钉钉服务(mjava-pro 容器)
+ *
+ * <p>capability: multi-tenant-runtime。从 {@link TenantContext} 取当前租户的钉钉凭据,
+ * 自动走 {@code {tenantId}:dingtalk:{appKey}} 命名空间缓存 access_token,
+ * 避免不同租户互相覆盖。</p>
+ *
+ * <p>调用方范式:业务先 {@code @Autowired DynamicDDService},
+ * 再用 {@link #getAccessToken()} 拿到当前租户 token,传给具体 {@code DDClient_*} 方法。</p>
+ */
+@Slf4j
+@Service
+public class DynamicDDService {
+
+    private static final String VENDOR = "dingtalk";
+    private static final String TOKEN_KEY_FMT = "%s:" + VENDOR + ":%s";
+    // 钉钉 access_token 官方 TTL 7200s;UtilToken.put 内部会再扣 5s 容错
+    private static final long DEFAULT_TOKEN_TTL_MS = 7200_000L;
+
+    @Autowired
+    private DDClient ddClient;
+
+    /**
+     * 当前租户的钉钉凭据。租户/凭据缺失抛 {@link McException}。
+     */
+    public VendorCredential currentCredential() {
+        TenantProfile profile = TenantContext.current();
+        if (profile == null) {
+            throw new McException("TENANT_REQUIRED", "TenantContext 未注入,无法获取钉钉凭据");
+        }
+        VendorCredential credential = profile.credential(VENDOR);
+        if (credential == null) {
+            throw new McException("VENDOR_NOT_CONFIGURED",
+                    "tenant=" + profile.getTenantId() + " 未配置 dingtalk 凭据");
+        }
+        return credential;
+    }
+
+    /**
+     * 当前租户 access_token,按 {tenantId}:dingtalk:{appKey} 命名空间缓存。
+     */
+    public String getAccessToken() {
+        VendorCredential credential = currentCredential();
+        String key = String.format(TOKEN_KEY_FMT, TenantContext.currentTenantId(), credential.getAppKey());
+        String token = UtilToken.get(key);
+        if (StringUtils.isNotBlank(token)) {
+            return token;
+        }
+        token = ddClient.getAccessToken(credential.getAppKey(), credential.getAppSecret());
+        if (StringUtils.isBlank(token)) {
+            throw new McException("DD_TOKEN_FAILED",
+                    "tenant=" + TenantContext.currentTenantId() + " 钉钉 token 获取失败");
+        }
+        UtilToken.put(key, token, DEFAULT_TOKEN_TTL_MS);
+        return token;
+    }
+
+    public String currentAppKey() {
+        return currentCredential().getAppKey();
+    }
+
+    public String currentAppSecret() {
+        return currentCredential().getAppSecret();
+    }
+
+    public String currentCorpId() {
+        return currentCredential().getCorpId();
+    }
+}

+ 0 - 131
openspec/BACKLOG.md

@@ -1,131 +0,0 @@
-# mjava-ai 待办看板
-
-> 更新:2026-04-26
-> 范围:跨 change 的待办池 + 每 change 内部进度汇总
-> 用途:按优先级顺序执行;完成一项立即更新对应 `tasks.md` 和本文件
-
-## 执行阶段
-
-| 阶段 | 内容 | 当前状态 |
-|------|------|---------|
-| A | 开发规范 + 待办清单(文档层) | 进行中 |
-| B.1 | 代码优化:补全 mjava 模块 API 能力 | 待启动 |
-| B.2 | 代码优化:待办功能添加(非 pro/com 的基座增强) | 待启动 |
-| C | mjava-pro / mjava-com 两个专项子模块开发 | 待 B 完成后启动 |
-
-## 阶段 A 待办
-
-| ID | 任务 | 状态 |
-|----|------|------|
-| A1 | 开发规范 11 章(mjava-baseline.md)写到 §3.5 审计、§3.4 Client/Service 分层 | ✅ 完成 |
-| A2 | yida-serverside.md 从原 CLAUDE.md 拆出 | ✅ 完成 |
-| A3 | 后端/CLAUDE.md 改为总索引 | ✅ 完成 |
-| A4 | 上层 claude/CLAUDE.md 同步索引 | ✅ 完成 |
-| A5 | server 目录分层 cur/pre | ✅ 完成 |
-| A6 | mjava-ai 仓库 git init + 首次 commit + push | ✅ 完成 |
-| A7 | 敏感文件 .gitignore 防护 + .example 模板 | ✅ 完成 |
-| A8 | mjava-pro / mjava-com 专项提案四件套 | ✅ 完成 |
-| A9 | init-project-baseline tasks 5.1-5.3 验证 | ✅ 完成 |
-| A10 | 归档 `extract-dingtalk-standard-api` | ✅ 完成,`changes/archive/2026-04-18-extract-dingtalk-standard-api/` |
-| A11 | `brew install maven` 后补做 init-baseline 任务 5.4(编译冒烟) | ⏳ 阻塞在本机未装 Maven |
-| A12 | `/Users/malk/Desktop/Tech/claude/` git init(规范文档版本化) | ✅ 完成(白名单策略,只收规范 md) |
-| A13 | 扩展 `mjava-baseline.md §3.4` — Client API 对齐详细规则(§3.4.1-§3.4.5)| ✅ 完成 |
-
-## 阶段 B.1 — 补全 mjava 模块 API 能力
-
-按用户优先级指令(2026-04-18):**先宜搭,后钉钉通讯录,其余 vendor 暂不做**。改造行为由 `/opsx:apply` 触发,本阶段**先完成规则定义**。
-
-| ID | 任务 | 归属 change | 状态 |
-|----|------|------------|------|
-| B1.1 | **宜搭**表单 + 流程 API 对齐规则与覆盖度矩阵 | `changes/extend-yida-api-coverage/` | ✅ 规则就绪(proposal/design/spec/tasks 四件套 valid) |
-| B1.2 | **钉钉通讯录** API 对齐规则与覆盖度矩阵 | `changes/extend-dingtalk-contacts-api/` | ✅ 规则就绪 |
-| B1.3 | 钉钉其余模块(考勤/审批/消息/群聊/...)| 未立项 | ⏸ 暂缓,待用户指令 |
-| B1.4 | 其他 vendor(北森/teambition/fxiaoke/h3yun/vika/xbongbong/feishu/ekuaibao)| 未立项 | ⏸ 低优先级,按需补 |
-| B1.7 | **集成平台**用户域 API(鉴权 + create/update/delete/query) | `changes/archive/2026-04-26-add-integration-user-api/` → `specs/integration-user-api/` | ✅ 2026-04-26 归档(mvn package 7 模块通过;联调冒烟由首个真实接入客户做) |
-| B1.5 | **实施**:宜搭原子 Client 落地(`/opsx:apply extend-yida-api-coverage`) | — | ⏳ 待用户触发 |
-| B1.6 | **实施**:钉钉通讯录原子 Client 落地(`/opsx:apply extend-dingtalk-contacts-api`) | — | ⏳ 待用户触发 |
-
-## 阶段 B.2 — 待办功能添加(基座增强)
-
-不依赖 pro/com 专项的基座功能增强,都先做在 `mjava/` 基座里,让三个现存客户模块可立刻受益。
-
-| ID | 任务 | 归属 change | 备注 |
-|----|------|-------------|------|
-| B2.1 | `UtilHttp` 内部落实 §3.5 审计日志(请求/响应脱敏 + latency + success) | 待立项 | 脱敏规则:token/appSecret/password/aesKey/privateKey → `***` |
-| B2.2 | `UtilToken` 支持 namespace 参数(为 mjava-pro 租户隔离铺路) | 待立项 | API:`put(namespace, key, value, ttl)` |
-| B2.3 | `CallerRateLimiter` 抽象(通用限流组件) | 待立项 | Guava RateLimiter 封装 |
-| B2.4 | 完成 `add-observability-foundation` tasks 4.2 / 5.2(生产冒烟) | observability | 需 Maven |
-| B2.5 | 把 Client 方法兼容全参数的**代码审查清单**(checklist)沉淀进 `/opsx:propose` 的模板 | 未定,文档侧 | 避免将来 PR 漏传字段 |
-| B2.6 | 请求监听的 trace 输出示例 | 文档 | baseline §3.5 / §8 |
-| **B2.7** | **基座请求鉴权 + 防重放**(HMAC-SHA256 + 时间窗 + Nonce) | `changes/add-request-auth-replay-guard/` | ✅ 规则就绪,0/33 任务;**mjava-com 的前置依赖** |
-
-## 阶段 C — 专项子模块开发(最后启动)
-
-| 专项 | tasks.md 进度 |
-|------|--------------|
-| `add-mjava-pro/` | 0 / 30 |
-| `add-mjava-com/` | 0 / 30 |
-
-按 tasks.md 依序走 `/opsx:apply`。
-
-## 进度总览(2026-04-19 更新)
-
-```
-✅ Phase A 完成(文档体系 + server 分层 + git 初始化)
-✅ Phase B.1 extend-yida-api-coverage       代码落地 + mvn compile 通过
-✅ Phase B.1 extend-dingtalk-contacts-api   代码落地 + mvn compile 通过
-✅ Phase B.2 add-request-auth-replay-guard  代码落地 + mvn compile 通过
-✅ Phase C   add-mjava-pro                  骨架 + mvn compile 通过(仍 active,15/30)
-✅ Phase C   add-mjava-com                  骨架 + mvn compile 通过(仍 active,19/30)
-✅ 全 reactor 6 模块 mvn package -DskipTests 全部打包成功(2026-04-19)
-
-归档(7 changes,稳态 spec 已合并到 openspec/specs/):
-✅ 2026-04-18 extract-dingtalk-standard-api → crypto-utils
-✅ 2026-04-19 init-project-baseline         → project-baseline.md
-✅ 2026-04-19 extend-yida-api-coverage      → yida-form-atomic + yida-process-atomic
-✅ 2026-04-19 extend-dingtalk-contacts-api  → dingtalk-contacts-v2
-✅ 2026-04-19 add-request-auth-replay-guard → request-auth + replay-guard
-✅ 2026-04-26 add-integration-user-api      → integration-user-api
-
-📋 剩余:
-- add-observability-foundation 2 项运行冒烟(java -jar 启动验证 actuator)
-- ✅ 各 change 的子模块 .example yml 扩展 mjava.auth 段(2026-04-26)
-- ✅ logback pattern 追加 %X{authKey:-} / %X{tenantId:-} / %X{callerId:-}(2026-04-26)
-- DingtalkActionRegistry / AliworkActionRegistry 首批 action 注册(按需)
-- 覆盖度矩阵官方文档人工复核(yida/dingtalk)
-- 单元 / 集成测试
-```
-
-## 实施中风险记录
-
-### add-request-auth-replay-guard(2026-04-19 实施)
-
-1. **Body 二次读取**:`AuthInterceptor.readBody` 直接 `request.getReader()`,读完后 Controller 再读会拿空。生产启用前需加 `ContentCachingRequestWrapper`(Filter 层包装),否则 Controller 的 `@RequestBody` 会失效。记 BACKLOG 明日修(或随首个实际启用 enabled 的客户同时做)。
-2. **logback pattern**:未在基座 logback-spring.xml 追加 `[%X{authKey:-}]`。默认 enabled=false 下无影响;启用时记得加。
-3. **单元 + 集成测试**:阻塞 Maven。
-4. **NonceCache 并发**:用 `synchronized putIfAbsent`,高 QPS 单实例可能成瓶颈。当前单点场景足够,多实例部署时评估 Redis。
-5. **编译未验证**:本机无 Maven。`McR.error(String, String)` 签名已核对;`HandlerInterceptorAdapter` 在 Spring Boot 2.2 可用(Spring 5.3+ deprecated 但仍可编译)。
-
-### extend-dingtalk-contacts-api(2026-04-19 实施)
-
-1. **URL 路径待官方复核**:
-   - `/topapi/role/add_role` vs `/topapi/role/addrole` 钉钉历史版本命名不一(underscored vs kebab vs camel),实施前需对照官方文档逐条核准
-   - `/topapi/v2/department/listparentbydept` 推断
-   - `/topapi/hide_field/*` 推断(可能实际路径为 `/topapi/contact/empAttr/visibility/...`)
-2. **dept_id_list 格式**:`createUser_v2` 用 `String.join(",", ...)` 转逗号串。钉钉部分接口接受数组 `List<Long>`,部分接受逗号字符串;需对照官方文档确认。若官方要求数组,删掉 join 直接传 List。
-3. **编译未验证**:本机无 Maven;`DDR.isSuccess()` 假定存在(从 `DDR.getResult()` 推断),若实际无此方法 `deleteDepartment` / `deleteRole` / 批量分配等方法返回值判定会编译错。
-4. **Role 模块返回结构**:`listRoles` / `listRoleEmployees` 返回 Map 含 `list` / `page_cursor` / `has_more` 等字段,调用方需理解翻页;未做便利封装。
-
-### extend-yida-api-coverage(2026-04-18 实施)
-
-1. **官方文档 fetch 失败**:WebFetch 拿到的是索引页,URL 路径基于训练知识 + 旧 `YDClientImpl` 已验证 endpoint 推断。以下路径**未经人工官方文档对照**,冒烟前需复核:
-   - `/processes/tasks/{agree|disagree|redirect|cc|comment}`
-   - `/processes/instances/{terminate|revoke|redirect}`
-   - `/processes/operationRecords`(listApprovalRecords 推断)
-2. **编译未验证**:本机无 Maven,Java 代码未 `mvn compile`。潜在风险:
-   - `x-acs-dingtalk-access-token` header key 是否与 `ddClient.initTokenHeader()` 返回一致未验证
-   - `DDR_New.getResult()` 返回 `Object`,各处 cast 未实地测试
-   - `UtilHttp.doPut/doDelete` 的 4 参重载确认
-3. **Boolean 返回判定**:宜搭多数写操作返回 `true/false`,实现用 `Boolean.TRUE.equals(r.getResult())` 判定,但部分 endpoint 实际返回 Map 或数字时可能错判;实施冒烟时需抽样确认。
-4. **listFormIds** URL + body 都传 appType/formUuid,沿用旧实现逻辑,可能冗余但不影响正确性。
-5. **YDService 迁移**:本阶段不切换 YDService 内部调用到新原子方法(tasks 5.x 标为"可选不做"),等编译通过后再决定。

+ 0 - 93
openspec/changes/add-mjava-com/design.md

@@ -1,93 +0,0 @@
-## 依赖声明
-
-**前置依赖**:`add-request-auth-replay-guard`(基座 `UtilSignature` / `NonceCache` 已落地)
-
-mjava-com 的 `CallerAuthInterceptor` **不重新实现** HMAC 签名与 Nonce 去重,而是直接调用基座提供的 `UtilSignature.sign()` 与 `NonceCache.putIfAbsent()`;本 change 只负责"按 callerId 动态查 secret"与"按 callerId 限流"两层。
-
-## 架构要点
-
-### 请求流程
-
-```
-External System → POST /api/com/{vendor}/{action}
-                  Header: X-Caller-Id: {callerId}
-                          X-Signature: {HMAC-SHA256(callerSecret, body+timestamp)}
-                          X-Timestamp: {unix ms}
-                  Body: { ...params aligned with base Client method args }
-
-     ↓
-CallerAuthInterceptor
-  ├── 校验 X-Timestamp 5 分钟内
-  ├── CallerRegistryService.get(callerId) → callerSecret
-  ├── 计算 HMAC 对比
-  └── 通过 → 继续;失败 → 401
-     ↓
-GatewayController
-  ├── 查 ActionWhitelist:{vendor}.{action} 是否开放
-  ├── 用反射或注册的 handler 调对应基座 Client 方法
-  └── 包 McR 返回
-```
-
-### 调用方注册(宜搭权限表单)
-
-字段约定:
-
-| 宜搭字段 | 含义 |
-|---------|------|
-| `textField_callerId` | 调用方唯一 ID(签发时生成,如 `caller-YDCBC-001`) |
-| `textField_callerSecret` | HMAC 密钥(生成时返回给调用方,服务端也存,需加密字段) |
-| `textField_callerName` | 可读名称(便于审计) |
-| `textareaField_allowedActions` | JSON 数组:`["dingtalk.user.get", "aliwork.form.save"]`,精确白名单 |
-| `numberField_rateLimit` | 每秒限流上限 |
-| `radioField_enabled` | `on` / `off` |
-| `dateField_expireAt` | 密钥过期时间(可选) |
-
-### Vendor Action 路由
-
-为避免反射开销和控制边界,每个开放的 action 在代码里显式注册:
-
-```java
-@Component
-public class DingtalkActionRegistry {
-    @PostConstruct
-    public void register(ActionRegistry reg) {
-        reg.register("dingtalk.user.get", (ctx, body) -> {
-            DDConf conf = resolveConf(ctx);  // 可选:按 caller 配置不同 dingtalk 应用
-            return ddClient.getUserDetail(conf, body.getString("userid"));
-        });
-    }
-}
-```
-
-`application.yml` 只维护"哪些 action 默认开放"的白名单;细粒度调用方权限通过宜搭权限表单的 `allowedActions` 限制。
-
-### 限流
-
-- 本地 Guava `RateLimiter` 按 callerId 维度
-- 单实例场景足够;若未来多实例部署再评估 Redis / 分布式限流
-- 超限返回 `429 { code: "RATE_LIMITED" }`
-
-### 审计
-
-继承 `mjava-baseline §3.5` 审计规范,追加字段:
-
-| 字段 | 说明 |
-|------|------|
-| `callerId` | 调用方 ID |
-| `vendorAction` | `{vendor}.{action}` |
-| `signatureValid` | 签名校验结果 |
-
-日志输出 `./log/{日期}/com-{callerId}.log`(按调用方分片)。
-
-## 安全要点
-
-- `callerSecret` 存宜搭"加密字段"类型;内存访问后**不得**回传前端或记录到日志
-- HMAC 防重放:`X-Timestamp` 5 分钟外拒绝 + 可选 Nonce(Phase 1 不做,5 分钟窗口足够内部场景)
-- 每次调用尝试失败都打 `WARN`,连续 5 次同 callerId 失败触发 `ERROR` 告警(`ExceptionNotice` 已有通道)
-
-## Non-Goals(设计边界)
-
-- 不做 REST 规范化(不强制 HATEOAS / JSON:API)
-- 不做 WebSocket / 长连接
-- 不做异步任务回调(请求同步完成,调用方需要异步能力自行轮询或用 mjava-pro)
-- 不做请求转发到非 mjava vendor(本质是 mjava Client 方法的 HTTP 门面)

+ 0 - 53
openspec/changes/add-mjava-com/proposal.md

@@ -1,53 +0,0 @@
-> 状态(2026-04-18 立项):**提案阶段,未实施**
-> 优先级:高。与 `add-mjava-pro` 并行。
-
-## Why
-
-`mjava` 基座沉淀了大量通用能力:HTTP 请求封装(`UtilHttp`)、Token 缓存(`UtilToken`)、JPA + QueryDSL、统一响应体 `McR`、多数据源、TraceId 链路 等。目前这些能力只能被"引入 mjava 依赖"的 Java 子项目(mcli/shunfeng/guangming)使用。
-
-但实际业务中常遇到:
-
-- **非 Java 系统**(Node.js / Python / PHP 宜搭连接器 / 低代码平台)需要调用钉钉/宜搭 API,每个系统重复实现鉴权 + HTTP + token 缓存,浪费且容易出错
-- **前端侧业务**(宜搭自定义页面 JSX)也希望复用后端对接能力,不想在前端暴露 appSecret
-- **实习/新人项目**没必要为了调用一次钉钉 API 就起一个完整的 mjava 子项目
-
-需要把 mjava 基座能力"服务化"——通过 `mjava-com` 子模块对外开放 REST API,外部系统只要通过鉴权就能直接调用钉钉/宜搭/飞书等底层接口,**无需引入 mjava 代码**。
-
-## What Changes
-
-新增子模块 `mjava-com/`,具备:
-
-- **统一入口 API**:`/api/com/{vendor}/{action}`(如 `POST /api/com/dingtalk/user.get`)转发到基座 Client 方法
-- **调用方鉴权**:从宜搭"权限表单"(formUuid 配置在 `application.yml`)查询被授权的调用方白名单,按 apiKey + signature 验签
-- **审计日志**:每次调用打完整审计(调用方 ID + vendor + action + latency + success)
-- **限流/熔断**:按调用方维度做基础限流(避免某个外部系统打爆第三方配额);Phase 1 用本地 `RateLimiter`,不引入 Sentinel/Resilience4j
-- **Vendor 暴露白名单**:不是所有 Client 方法都自动开放,需要在 `application.yml` 显式声明哪些 `{vendor}.{action}` 可被 com 调用
-
-## Capabilities
-
-### New Capabilities
-- `baas-gateway`:BaaS 风格的后端即服务网关,把 mjava Client 能力以 REST 开放
-- `caller-registry`:外部调用方注册中心(宜搭权限表单)
-
-### Modified Capabilities
-<!-- 基座 Client 层保持不变,com 作为 facade 层向外暴露 -->
-
-## Impact
-
-- **新增模块**:`mjava-com/`(独立 jar,默认端口 9020,context-path `/api/com`)
-- **新增代码**(预计):
-  - `CallerAuthInterceptor.java`(apiKey + signature 验签)
-  - `CallerRegistryService.java`(查宜搭权限表单)
-  - `GatewayController.java`(路由 `/{vendor}/{action}` 到基座 Client)
-  - `ActionWhitelistConfig.java`(vendor.action 白名单)
-  - 简单本地限流 `CallerRateLimiter.java`
-- **基座影响**:零(com 作为 facade 调基座现有 Client)
-- **外部调用方**:简化 — 只需 apiKey + signature + JSON body 发 POST 即可
-
-## Non-Goals
-
-- ❌ 不做 OpenAPI / Swagger 规范(第一阶段人工维护接口文档;若需要再单独提案)
-- ❌ 不做计费 / 配额套餐(Phase 1 固定限流即可)
-- ❌ 不做 OAuth2 授权(apiKey + HMAC 签名足够内部场景;跨组织再评估)
-- ❌ 不引入 API Gateway 中间件(Kong / APISIX / Spring Cloud Gateway 等)
-- ❌ 不做请求录制回放(可测性工作,独立专项)

+ 0 - 75
openspec/changes/add-mjava-com/specs/baas-gateway/spec.md

@@ -1,75 +0,0 @@
-## ADDED Requirements
-
-### Requirement: 统一入口
-
-mjava-com SHALL 提供单一网关路由 `POST /api/com/{vendor}/{action}`,每个 action MUST 对应基座某个 Client 方法。
-
-#### Scenario: vendor + action 路由
-
-- **WHEN** 收到 `POST /api/com/dingtalk/user.get`
-- **THEN** 根据 `ActionRegistry` 查找 `dingtalk.user.get` 注册的 handler
-- **AND** 调用基座 `DDClient_Contacts.getUser_v2(...)`
-- **AND** 请求体字段名必须与 Client 方法参数一致
-
-### Requirement: 签名校验
-
-网关 SHALL 在 Auth 拦截阶段校验调用方签名。校验逻辑 MUST 复用基座 `add-request-auth-replay-guard` 提供的 `UtilSignature` 与 `NonceCache`,不重新实现 HMAC。
-
-#### Scenario: 四层校验顺序
-
-- **WHEN** 请求到达
-- **THEN** 依次执行:时间戳窗口 → callerId 注册表查 secret → HMAC-SHA256 比对 → 调用方 enabled
-- **AND** 任一失败返回 `401 { code: "AUTH_FAILED" }`
-- **AND** 响应 message 不暴露具体失败环节
-
-### Requirement: 动作白名单
-
-每个开放的 action MUST 通过三层校验:ActionRegistry 代码注册 + yml `com.actions.enabled` 启用 + 调用方 `allowedActions` 包含。
-
-#### Scenario: 代码未注册
-
-- **WHEN** 请求 action 未在 `ActionRegistry` 注册
-- **THEN** 返回 `404 { code: "ACTION_NOT_FOUND" }`
-
-#### Scenario: yml 未启用
-
-- **WHEN** `com.actions.enabled` 不含该 action
-- **THEN** 返回 `403 { code: "ACTION_FORBIDDEN" }`
-
-#### Scenario: 调用方无权限
-
-- **WHEN** callerId 的 `allowedActions` 不含该 action
-- **THEN** 返回 `403 { code: "ACTION_FORBIDDEN" }`
-
-### Requirement: 响应格式
-
-网关 SHALL 统一返回 `McR<T>`。第三方原始响应 MUST 放在 `data` 字段保留原结构。
-
-#### Scenario: 成功透传
-
-- **WHEN** 基座 Client 返回成功
-- **THEN** `McR.success(result)`,`data = 原始 Map 或 DTO`
-
-#### Scenario: 第三方失败映射
-
-- **WHEN** 基座 Client 抛 McException(vendor 返回错误)
-- **THEN** `McR.fail(code, message)`,`data` 含 vendor 原始错误信息
-
-### Requirement: 限流
-
-网关 SHALL 按 callerId 做本地限流,阈值取自宜搭权限表单 `rateLimit` 字段。
-
-#### Scenario: 超限拒绝
-
-- **WHEN** 同一 callerId 请求速率超过配置 QPS
-- **THEN** 返回 `429 { code: "RATE_LIMITED", retryAfter: 1 }`
-
-### Requirement: 审计扩展
-
-网关审计日志 MUST 在 `mjava-baseline §3.5` 字段基础上追加 callerId / vendorAction / signatureValid。
-
-#### Scenario: 每请求按 caller 分片
-
-- **WHEN** 请求进入
-- **THEN** 日志落入 `./log/{日期}/com-{callerId}.log`
-- **AND** MDC 含 `callerId` 与 `vendorAction`

+ 0 - 48
openspec/changes/add-mjava-com/specs/caller-registry/spec.md

@@ -1,48 +0,0 @@
-## ADDED Requirements
-
-### Requirement: 注册源
-
-调用方白名单 SHALL 维护于宜搭"权限表单"。mjava-com MUST 通过 `com.caller.registry.formUuid` 配置项动态加载调用方清单。
-
-#### Scenario: 启动时全量加载
-
-- **WHEN** 应用启动 `@PostConstruct`
-- **THEN** 按 `com.caller.registry.formUuid` 分页查询宜搭表单
-- **AND** 过滤 `enabled=on` 的记录转为 `CallerProfile` 写入内存 `Map`
-
-### Requirement: 加载与刷新
-
-注册表 SHALL 支持 TTL 自动刷新与 stale-while-revalidate。
-
-#### Scenario: TTL 到期触发刷新
-
-- **WHEN** 距上次加载超过 `com.caller.registry.ttlSeconds`(默认 300 秒)
-- **THEN** 异步重新拉取宜搭表单
-- **AND** 期间旧数据继续可用,直到新数据就绪替换
-
-#### Scenario: 手动刷新入口
-
-- **WHEN** 调用 `POST /api/com/_admin/reloadCallers`
-- **AND** 当前 profile 为 dev
-- **THEN** 立即同步重新加载
-
-### Requirement: 密钥轮换
-
-调用方密钥更新 SHALL 由运营在宜搭表单手动完成。本 change 不支持双密钥并存。
-
-#### Scenario: 单密钥轮换
-
-- **WHEN** 运营更新宜搭表单 `callerSecret`
-- **THEN** 下次 TTL 刷新后新密钥生效
-- **AND** 轮换期间客户端必须同步切换(可接受短暂 401)
-
-### Requirement: 调用方停用
-
-调用方停用 SHALL 在下次注册表刷新后立即生效。停用期间请求 MUST 返回 401。
-
-#### Scenario: enabled=off
-
-- **WHEN** 宜搭表单 `radioField_enabled=off`
-- **AND** 注册表刷新完成
-- **THEN** 该 callerId 从内存 Map 移除
-- **AND** 后续请求返回 `401 { code: "CALLER_DISABLED" }`

+ 0 - 53
openspec/changes/add-mjava-com/tasks.md

@@ -1,53 +0,0 @@
-## 1. 模块脚手架
-
-- [x] 1.1 新建 `mjava-com/`(非直接 cp mcli)
-- [x] 1.2 pom.xml artifactId=mjava-com,仅依赖 mjava
-- [x] 1.3 Boot.java package=com.malk.apigw(避免 com.malk.com 歧义)
-- [x] 1.4 application.yml port=9020 + context=/api/com
-- [x] 1.5 根 pom 追加 mjava-com module
-- [x] 1.6 `mvn -pl mjava-com -am compile` 通过(全 reactor 6 模块)
-
-## 2. 调用方鉴权
-
-- [x] 2.1 `CallerProfile` 数据类(callerId/secret/allowedActions/rateLimit/expireAt/enabled)
-- [x] 2.2 `CallerRegistryService` 查宜搭权限表 + 启动全量 + @Scheduled 定时刷新
-- [x] 2.3 `CallerAuthInterceptor`:Header 齐全 → 时间窗 → callerId 查 + 启用/过期 → NonceCache → HMAC → 限流,**复用基座 UtilSignature + NonceCache**
-- [x] 2.4 `ComWebConfig` 注册拦截器,排除 /_admin/** 等
-
-## 3. 动作注册
-
-- [x] 3.1 `ActionRegistry` Map<String, ActionHandler>
-- [x] 3.2 `ActionHandler` 函数式接口 `McR handle(CallerProfile, JSONObject)`
-- [ ] 3.3 `DingtalkActionRegistry` 首批 3 action(延后,等首个调用方接入时按需补)
-- [ ] 3.4 `AliworkActionRegistry` 首批 3 action(同上)
-- [x] 3.5 `CallerRegistryProperties.Actions` 读 com.actions.enabled 全局白名单
-
-## 4. 网关 Controller
-
-- [x] 4.1 `GatewayController` 路由 POST /{vendor}/{action}
-- [x] 4.2 三层校验(ActionRegistry 代码注册 / yml enabled / caller.allowedActions)
-- [x] 4.3 失败返回 McR.error(ACTION_NOT_FOUND / ACTION_FORBIDDEN / AUTH_FAILED / RATE_LIMITED / VENDOR_ERROR)
-
-## 5. 限流
-
-- [x] 5.1 `CallerRateLimiter` Guava RateLimiter per callerId(ConcurrentHashMap 存;rate 变更自动重建)
-- [x] 5.2 拦截器内直接调 rateLimiter.tryAcquire,429 RATE_LIMITED
-
-## 6. 审计日志
-
-- [x] 6.1 CallerAuthInterceptor 写 MDC `callerId` + 失败分支 log.warn(point logger)
-- [x] 6.2 logback pattern 追加 `[%X{callerId:-}]`(顺带补齐 `[%X{authKey:-}]` 与 `[%X{tenantId:-}]`)— 2026-04-26
-
-## 7. 配置与文档
-
-- [x] 7.1 `application-dev.yml.example`(com.caller.registry.* + com.actions.enabled 示例)— 2026-04-26
-- [x] 7.2 `application-prod.yml.example`(同上占位)— 2026-04-26 复核已存在
-- [ ] 7.3 `README.md`:外部系统对接步骤、signature 算法示例(Python / Node.js 样例代码)
-- [ ] 7.4 更新 `mjava-baseline.md` 表格里 mjava-com 状态为"已实施"
-
-## 8. 验证
-
-- [ ] 8.1 单元测试:`HmacSignatureTest` / `CallerRegistryTest`
-- [ ] 8.2 集成冒烟:模拟外部 Python 客户端签名 → 调 `/dingtalk/user.get` → 验证成功 & 失败(签名错、过期、限流、未启用 action)
-- [ ] 8.3 压测:单个 caller 限流准确性(RateLimiter 容差)
-- [ ] 8.4 `/opsx:validate add-mjava-com --strict` 通过

+ 0 - 75
openspec/changes/add-mjava-pro/design.md

@@ -1,75 +0,0 @@
-## 架构要点
-
-### 租户识别
-
-```
-Request → TenantInterceptor (读 X-Tenant-Id) → TenantContext.set(tenantId) → Controller → Service → Client
-                                                                                        ↓
-                                                                       Client 从 TenantContext 拿 appKey/appSecret
-```
-
-`TenantContext` 用 `ThreadLocal<TenantProfile>`;`TenantProfile` 包含:
-- `tenantId`(宜搭应用表主键)
-- `vendorCredentials: Map<String, VendorCredential>`(钉钉/aliwork 等)
-- `extraJson`(tenant 级自定义配置)
-
-### 租户配置加载
-
-**数据源**:宜搭"应用表"(formUuid 配置)。字段约定:
-
-| 宜搭字段 | 含义 |
-|---------|------|
-| `textField_tenantId` | 租户唯一标识(建议用客户代号如 `guangming`) |
-| `textField_vendor` | 第三方平台 key(`dingtalk` / `aliwork` / ...) |
-| `textField_appKey` | 该 vendor 的 appKey |
-| `textField_appSecret` | 该 vendor 的 appSecret(写入时走宜搭加密字段) |
-| `textField_corpId` | 可选(钉钉需要) |
-| `textareaField_extraJson` | JSON 字符串,扩展参数 |
-| `radioField_enabled` | `on` / `off` |
-
-**加载策略**:
-- 启动时全量拉取 → 内存 `Map<tenantId, TenantProfile>`
-- TTL 10 分钟(可配 `tenant.registry.ttlSeconds`)过期后按需刷新
-- 手动 `/pro/_admin/reloadTenant?tenantId=xxx`(仅 dev profile 开放)做热刷
-
-### Client 透传机制
-
-基座 `DDClient` 签名不变(仍接受 appKey/appSecret 参数),`mjava-pro` 提供一层 `DynamicDDService` 组合:
-
-```java
-// Service 层内部
-DDConf conf = TenantContext.current().credential("dingtalk").toDDConf();
-ddClient.listDepartmentUserDetail(conf, deptId, cursor, size);
-```
-
-**不做** AOP 反射修改签名 — 保持显式、可读、可调试。
-
-### Token 缓存隔离
-
-`UtilToken` 当前 key 是裸 `{vendor}:{appKey}`。多租户环境下两个客户可能共用同一 appKey(极少,但要防撞)。建议基座支持 namespace:
-
-```java
-UtilToken.put(tenantId, "dingtalk:" + appKey, token, expireSec);
-UtilToken.get(tenantId, "dingtalk:" + appKey);
-```
-
-若基座改动太大,**回退方案**:`mjava-pro` 约束 key 为 `{tenantId}:{vendor}:{appKey}`,不改基座。
-
-### 错误处理
-
-- tenantId 缺失 → `401 TENANT_REQUIRED`
-- tenantId 不存在或 disabled → `403 TENANT_NOT_FOUND`
-- vendor 配置缺失 → `500 TENANT_VENDOR_MISCONFIGURED` + 审计日志
-
-## 技术约束
-
-- 沿用 `mjava-baseline.md` 全部规范
-- `application-prod.yml` 只配宜搭应用表 formUuid / systemToken(访问宜搭自身的凭据仍是静态),不配任何客户凭据
-- 请求监听日志必须含 `tenantId` 字段(在 §3.5 审计字段基础上扩展)
-
-## Non-Goals(设计边界)
-
-- 不考虑租户级数据库隔离(所有租户共享同一 MySQL)
-- 不考虑租户级流量限额(QPS 限流等;有需求再单独提案)
-- 不考虑跨租户工作流(暂时假设每次请求单租户上下文)
-- 不考虑 Redis 分布式缓存(单实例 TimedCache 足够 Phase 1)

+ 0 - 57
openspec/changes/add-mjava-pro/proposal.md

@@ -1,57 +0,0 @@
-> 状态(2026-04-18 立项):**提案阶段,未实施**
-> 优先级:高。与 `add-mjava-com` 并行,共同承担"多用户/多客户使用支持"大方向。
-
-## Why
-
-现有 `mjava-mcli` / `mjava-shunfeng` / `mjava-guangming` 三个客户子项目采用"一客户一 jar"模式,每次加新客户都要:
-
-1. 复制 mcli 模板 → 改 artifactId / port / context-path
-2. 在 `application-{profile}.yml` 硬编码该客户的 appKey/appSecret/corpId
-3. 单独部署一个 JVM 实例
-
-这种方式在客户数量少(≤ 5)时尚可,但问题:
-
-- 新客户上线 = 发布新版本 + 运维新增部署节点,交付周期慢
-- 一客户一实例,资源利用率低
-- appKey/appSecret 静态写在 yml,切换客户需要重启 + 重新发包
-- 实例间没有共享缓存,每客户独立维护自己的 token 生命周期
-
-需要一个支持"**一个 jar 同时服务多个客户**"的子项目模板 `mjava-pro`,按请求上下文动态加载客户配置、token 按租户隔离缓存,**加客户 = 新增一条宜搭应用表记录**,无需改代码和重启。
-
-## What Changes
-
-新增子模块 `mjava-pro/`,具备以下能力:
-
-- **租户解析**:请求入口识别 `X-Tenant-Id` header(或 subdomain / path prefix,实现时选定),未识别到返回 401
-- **应用配置动态加载**:从宜搭"应用表"(formUuid 配置在 `application.yml`)按 tenantId 查询 appKey / appSecret / corpId / 其他授权参数;命中后缓存到内存(TTL 可配,默认 10 分钟)
-- **Token 隔离**:`UtilToken` key 扩展为 `{tenant}:{vendor}:{appKey}`,各租户独立缓存不互相覆盖
-- **Client 透传**:基座 `DDClient` / `YDClient` 等保持原签名不变;`mjava-pro` 内部通过 `TenantContext`(ThreadLocal)传 appKey/appSecret 进去
-- **配置表结构约定**:宜搭应用表字段规范固定在 `spec.md` 中(tenantId / vendor / appKey / appSecret / extraJson / enabled / ...)
-
-## Capabilities
-
-### New Capabilities
-- `multi-tenant-runtime`:单部署多租户运行时,按请求上下文路由到对应客户配置
-- `tenant-registry`:从宜搭应用表加载租户配置的注册中心抽象
-
-### Modified Capabilities
-<!-- 基座 Client 层签名保持不变,tenant context 作为 cross-cutting concern 注入 -->
-
-## Impact
-
-- **新增模块**:`mjava-pro/`(独立 jar,默认端口 9010,context-path `/api/pro`)
-- **新增代码**(预计):
-  - `TenantContext.java` + `TenantInterceptor.java`(ThreadLocal 管理)
-  - `TenantRegistryService.java`(查宜搭应用表)
-  - `DynamicDDClientProxy.java`(包装基座 DDClient,按 tenant 注入凭据)
-  - `application.yml` + `application-{dev,prod}.yml.example`
-- **基座影响**:可能需要 `UtilToken` 支持 key 命名空间;评估后若需要再开子提案
-- **现有客户模块**:零影响,mcli/shunfeng/guangming 继续运行
-- **部署**:单 pro jar 可接管 N 个客户,运维新增客户只在宜搭应用表加一行记录即可
-
-## Non-Goals
-
-- ❌ 不替代现有 mjava-mcli/shunfeng/guangming(已交付客户保持原架构)
-- ❌ 不引入 Redis(tenant 配置缓存仍走 `TimedCache`,单实例足够;若将来多实例再评估)
-- ❌ 不做跨租户数据聚合 / 租户级计费 / 多租户数据库分离
-- ❌ 本提案不涵盖前端租户切换 UI(前端按现有方式各自管理)

+ 0 - 61
openspec/changes/add-mjava-pro/specs/multi-tenant-runtime/spec.md

@@ -1,61 +0,0 @@
-## ADDED Requirements
-
-### Requirement: 租户识别
-
-mjava-pro SHALL 在请求入口识别租户上下文。Controller 入口 MUST 通过 `TenantInterceptor` 读 HTTP Header `X-Tenant-Id`。
-
-#### Scenario: Header 缺失
-
-- **WHEN** 请求缺少 `X-Tenant-Id`
-- **THEN** 返回 `401 { code: "TENANT_REQUIRED" }`
-
-#### Scenario: 租户不存在
-
-- **WHEN** Header 存在但租户不在注册表
-- **THEN** 返回 `403 { code: "TENANT_NOT_FOUND" }`
-
-#### Scenario: 识别成功
-
-- **WHEN** tenantId 匹配注册表记录且启用
-- **THEN** `TenantContext.set(profile)` 写入 ThreadLocal
-- **AND** 请求结束时在 finally 块 `TenantContext.clear()`
-
-### Requirement: 异步任务传递
-
-TenantContext MUST 在 @Async 线程池与 CompletableFuture 切换时正确传递,避免子线程拿不到租户凭据。
-
-#### Scenario: @Async 线程池
-
-- **WHEN** 通过 `@Async` 提交任务
-- **THEN** 线程池 TaskDecorator 必须复制 TenantContext 到子线程
-- **AND** 子线程开始执行时 `TenantContext.current()` 返回发起请求的租户
-
-#### Scenario: 手动线程切换
-
-- **WHEN** 代码显式切换线程(如 `CompletableFuture.supplyAsync(executor, ...)`)
-- **THEN** 必须使用 `TenantContext.propagate(ctx, runnable)` 包装
-
-### Requirement: Token 隔离
-
-mjava-pro SHALL 使用带租户前缀的 UtilToken key,避免多租户间 token 互相覆盖。
-
-#### Scenario: key 格式
-
-- **WHEN** 调用 UtilToken 存取
-- **THEN** key 格式统一为 `{tenantId}:{vendor}:{appKey}`
-- **AND** mjava-pro 代码 MUST 不使用无 tenant 前缀的 UtilToken 接口
-
-#### Scenario: 无租户上下文场景
-
-- **WHEN** 定时任务或启动时发起调用
-- **THEN** 必须显式传 tenantId,或声明为 `SYSTEM` 伪租户
-
-### Requirement: 审计日志扩展
-
-mjava-pro SHALL 在 `mjava-baseline §3.5` 字段基础上追加 tenantId,日志按租户分片。
-
-#### Scenario: 按租户分片输出
-
-- **WHEN** 请求进入
-- **THEN** 日志输出到 `./log/{日期}/pro-{tenantId}.log`
-- **AND** MDC 含 `tenantId` 字段

+ 0 - 54
openspec/changes/add-mjava-pro/specs/tenant-registry/spec.md

@@ -1,54 +0,0 @@
-## ADDED Requirements
-
-### Requirement: 注册源
-
-租户配置 SHALL 存放于宜搭"应用表"。mjava-pro MUST 通过 `tenant.registry.formUuid` 配置项加载租户列表。
-
-#### Scenario: 访问宜搭自身凭据
-
-- **WHEN** mjava-pro 启动加载租户表
-- **THEN** 使用 `application-{profile}.yml` 配置的 `aliwork.appType` / `aliwork.systemToken`(mjava-pro 自身入口凭据,不是租户凭据)
-- **AND** 租户凭据从宜搭表记录动态加载
-
-### Requirement: 加载与缓存
-
-租户注册表 SHALL 支持启动全量加载 + TTL 刷新 + stale-while-revalidate。
-
-#### Scenario: 启动全量加载
-
-- **WHEN** 应用启动 `@PostConstruct`
-- **THEN** 分页拉取全部 `enabled=on` 租户记录
-- **AND** 写入内存 `Map<String, TenantProfile>`
-
-#### Scenario: TTL 过期刷新
-
-- **WHEN** 距上次加载超过 `tenant.registry.ttlSeconds`(默认 600 秒)
-- **THEN** 异步重新拉取
-- **AND** 旧值继续可用直到新值就绪
-
-#### Scenario: 热刷入口
-
-- **WHEN** 调用 `POST /api/pro/_admin/reloadTenant?tenantId=xxx`
-- **AND** 当前 profile 为 dev
-- **THEN** 立即刷新单条记录
-
-### Requirement: 敏感字段处理
-
-租户凭据(appSecret / systemToken)MUST 在日志输出时脱敏。内存中允许保留明文。
-
-#### Scenario: 日志脱敏
-
-- **WHEN** 审计日志打印 TenantProfile
-- **THEN** appSecret / systemToken / accessKey 等字段输出为 `***`
-- **AND** tenantId / vendor / corpId 等非敏感字段正常输出
-
-### Requirement: 失效与禁用
-
-宜搭表 `enabled=off` 的租户 MUST 在下次刷新后移除。停用期间请求 MUST 返回 403。
-
-#### Scenario: 禁用生效
-
-- **WHEN** 宜搭表单 `radioField_enabled=off`
-- **AND** 注册表刷新完成
-- **THEN** 该 tenantId 从内存 Map 移除
-- **AND** 携带此 tenantId 的请求返回 `403 { code: "TENANT_DISABLED" }`

+ 0 - 50
openspec/changes/add-mjava-pro/tasks.md

@@ -1,50 +0,0 @@
-## 1. 模块脚手架
-
-- [x] 1.1 新建 `mjava-pro/`(非直接 cp mcli,精简依赖)
-- [x] 1.2 `pom.xml` artifactId=mjava-pro,依赖仅 mjava 基座
-- [x] 1.3 `Boot.java` package=com.malk.pro,`@EnableScheduling` + `@EnableJpaAuditing` + scanBasePackages=com.malk
-- [x] 1.4 `application.yml` port=9010 / context-path=/api/pro / spel.scheduling=true / multiSource=false
-- [x] 1.5 根 `pom.xml` `<modules>` 追加 `mjava-pro`
-- [x] 1.6 `mvn -pl mjava-pro -am compile` 通过(2026-04-19 验证)
-
-## 2. TenantContext 核心
-
-- [x] 2.1 `TenantProfile` + `VendorCredential` 数据类
-- [x] 2.2 `TenantContext`(ThreadLocal + set/current/clear/propagate + currentTenantId)
-- [x] 2.3 `TenantInterceptor`(preHandle 读 X-Tenant-Id → registry.get → TenantContext.set + MDC;afterCompletion clear;401 TENANT_REQUIRED / 403 TENANT_NOT_FOUND)
-- [x] 2.4 `ProWebConfig` 注册拦截器,排除 /_admin/** / /actuator/** / /static/** 等
-- [ ] 2.5 TenantTaskDecorator(@Async 传播)— 延后,首次生产启用时评估
-
-## 3. TenantRegistry
-
-- [x] 3.1 `TenantRegistryService`(依赖 `YDClient_Form`,使用新原子接口 searchForm)
-- [x] 3.2 `loadAll()` 按 formUuid 分页查宜搭应用表 → mergeRow 合并同 tenantId 多 vendor → `Map<String, TenantProfile>`
-- [x] 3.3 `get(tenantId)` / `contains` / `size` 内存 Map 直查(缓存 miss 不触发单条拉取,等下次 TTL 刷新)
-- [x] 3.4 `@PostConstruct init()` + `@Scheduled(fixedDelayString=ttl*1000)` 定时刷新;failFast 开关控制启动失败行为
-- [x] 3.5 `AdminController` `@Profile("dev") @NoAuth POST /_admin/reloadTenant`
-
-## 4. Client / Service 透传
-
-- [ ] 4.1 新建 `com.malk.pro.service.dingtalk.DynamicDDService`:从 `TenantContext` 读凭据 → 构造 `DDConf` → 调基座 `DDClient`
-- [ ] 4.2 同法建 `DynamicYDService`(对接宜搭)、其他 vendor 按需
-- [ ] 4.3 `UtilToken` 使用规范:key 统一 `{tenantId}:{vendor}:{appKey}`(若基座未扩展 namespace,就在调用侧拼)
-
-## 5. 审计日志
-
-- [ ] 5.1 `UtilHttp` 审计日志扩展字段 `tenantId`(从 MDC 读);改基座或在 pro 侧包一层
-- [ ] 5.2 logback-spring.xml 为 mjava-pro 独立输出 `./log/{日期}/pro-%X{tenantId}.log`
-- [ ] 5.3 确保异步线程能读到 MDC(配合 Task 2.5)
-
-## 6. 配置与文档
-
-- [x] 6.1 `application-dev.yml.example`(含 tenant.registry.* 占位)— 2026-04-26
-- [x] 6.2 `application-prod.yml.example`(含 tenant.registry.* 占位)— 2026-04-26 复核已存在
-- [ ] 6.3 `README.md`(在 mjava-pro 目录下,说明新增租户操作步骤)
-- [ ] 6.4 更新 `/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md` 表格里 mjava-pro 状态改为"已实施"
-
-## 7. 验证
-
-- [ ] 7.1 单元测试:`TenantContextTest`(set/clear/异步传播)
-- [ ] 7.2 集成冒烟:两个租户配置 → 用不同 `X-Tenant-Id` 调同一接口 → 验证 token 隔离 + 审计日志分片
-- [ ] 7.3 压测:100 QPS 下 TenantContext 无泄漏(ThreadLocal clear 正确)
-- [ ] 7.4 `/opsx:validate add-mjava-pro --strict` 通过

+ 0 - 2
openspec/changes/add-observability-foundation/.openspec.yaml

@@ -1,2 +0,0 @@
-schema: spec-driven
-created: 2026-04-14

+ 0 - 51
openspec/changes/add-observability-foundation/design.md

@@ -1,51 +0,0 @@
-## Context
-
-mjava 基座 (`com.malk`) 作为内部多业务脚手架,业务模块 (`mjava-guangming` / `mjava-shunfeng` 等) 靠继承基座获得标准能力。当前三块可观测性空白必须由基座一次性补齐,避免每个业务自建。
-
-关键约束:Spring Boot **2.2.13.RELEASE** + Java **1.8**,不能引入仅支持 SB 2.7+ 或 Java 11+ 的库。
-
-## Goals / Non-Goals
-
-**Goals**
-- 零破坏:业务模块不用改任何代码或配置(除非要自定义)
-- 默认安全:生产环境 Swagger 不开、actuator 只开 health/info
-- 一次接入:三件套在基座统一做,业务自动继承
-
-**Non-Goals**
-- 不给每个 Controller 加 `@Tag/@Operation` 注解(让文档自动按 Spring 注解生成即可)
-- 不接外部 APM / Skywalking(超出脚手架范围)
-- 不做分布式 traceId(与上游服务协议对接是业务侧事情;基座只做进程内 MDC)
-- 不加自定义 HealthIndicator(除非后续有真实需求)
-
-## Decisions
-
-### D1:Swagger 选 springdoc,不用 springfox
-- **理由**:springfox 3.0.0 对 Spring Boot 2.2 有已知的 `ExpandedParameterBuilderPlugin NullPointer` 告警,且社区已事实停维。springdoc-openapi 1.6.x 与 SB 2.2 完全兼容,是官方推荐替代。
-- **替代**:继续用 springfox —— 拒绝,告警影响启动日志清洁度,未来升级 SB 还要重做。
-- **具体版本**:`springdoc-openapi-ui:1.6.15`(兼容 SB 2.2 的末版系列)
-
-### D2:TraceId 用 Servlet Filter + MDC,不用 Spring Interceptor
-- **理由**:Filter 比 Interceptor 更早生效(在 Spring MVC 之前),静态资源、异常也能覆盖;MDC API 简单无依赖
-- **线程池透传**:`AsyncTaskExecutor` 通过 `TaskDecorator` 在提交前复制 MDC,避免 `@Async` 子任务丢 traceId
-
-### D3:健康检查默认只开 health/info
-- **理由**:env/beans/configprops 泄漏敏感配置(数据库密码路径、appSecret 等),默认必须关闭
-- **后续**:如需要 Prometheus 指标再单开 `management.endpoints.web.exposure.include=health,info,prometheus` + micrometer 依赖
-
-### D4:logback pattern 用 `[%X{traceId:-}]`(默认空字符串)
-- **理由**:非 HTTP 调用(定时任务、启动阶段)不走 filter,traceId 会不存在;用 `{:-}` 防止打印 `null`
-- **格式**:`%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-}] %-5level - [%method,%line] - %msg%n`
-
-## Risks / Trade-offs
-
-- **[风险] springdoc 引入后 /v3/api-docs 默认开放** → 通过 `springdoc.api-docs.enabled: ${swagger.enable:false}` 绑定同一开关
-- **[风险] actuator 路径撞上业务 `/api/actuator/*`** → 极低概率;如真撞可加 `management.endpoints.web.base-path: /mgmt`
-- **[风险] TaskDecorator 与已有 AsyncConfig 冲突** → 审读 `mjava/core/AsyncConfig.java` 现有配置后再追加,不删既有逻辑
-- **[代价] 启动类路径增大 ~5 MB** → 接受,对 jar 体积影响可忽略
-
-## Migration Plan
-
-1. 基座 pom 加依赖
-2. 写 3 个 Java 类 + 1 个 logback 修改 + 3 个 yml 行
-3. 本地启动 mjava-mcli 或 guangming 冒烟:访问 `/api/actuator/health`、`/api/swagger-ui.html`、查看日志是否带 traceId
-4. 业务模块升级基座版本号后重启即生效,不需要额外改动

+ 0 - 36
openspec/changes/add-observability-foundation/proposal.md

@@ -1,36 +0,0 @@
-> 状态(2026-04-18 重新索引):**代码实施完成,待生产冒烟验证**
-> tasks.md 中 4.2 健康端点启动冒烟、5.2 IDE 启动回归两项待执行;其余已 [x]。后续由 `/opsx:apply` 续做冒烟,完成后 `/opsx:archive`。
-
-## Why
-
-mjava 基座作为新业务的标准脚手架,当前有三个可观测性短板:
-1. **无 API 文档生成** —— `swagger.enable: false` 和 pom 里孤立的 springfox version 都说明"曾打算做但没落地",新成员无法自助理解接口
-2. **无请求链路 TraceId** —— logback pattern 没有 `%X{...}`,生产故障排查全靠时间戳+人肉定位
-3. **无健康检查端点** —— Docker/K8s 探针、运维监控没有抓手
-
-这三件都是"标准 Spring Boot 项目的最低可观测性配置",缺一样新业务就要自己补。应一次性在基座做完。
-
-## What Changes
-
-- 新增 `spring-boot-starter-actuator` 依赖;开放 `/actuator/health` 和 `/actuator/info`,其他端点默认关闭
-- 新增 `springdoc-openapi-ui` 1.6.x 依赖(选它而非 springfox 3:与 Spring Boot 2.2 兼容更稳,官方推荐路线);新增 `mjava/config/OpenApiConfig.java` 受 `swagger.enable` 开关控制
-- 新增 `mjava/filter/TraceIdFilter.java` + `TaskDecorator` 实现:从 header `X-Trace-Id` 读取或生成 UUID,写入 MDC 并回写响应头
-- 修改 `mjava/src/main/resources/logback-spring.xml` 日志 pattern 增加 `[%X{traceId}]` 字段
-- 移除 parent pom 中未使用的 `<springfox-boot-starter.version>` property(清理)
-
-## Capabilities
-
-### New Capabilities
-- `observability-foundation`: 基座可观测性三件套(OpenAPI 文档、TraceId 链路、Actuator 健康检查)
-
-### Modified Capabilities
-<!-- 无 -->
-
-## Impact
-
-- **pom 变更**:`mjava/pom.xml` 增加 actuator + springdoc 依赖;parent pom 清理 springfox 属性
-- **代码新增**:`OpenApiConfig.java` (~30 行)、`TraceIdFilter.java` (~40 行)、`AsyncTraceIdDecorator.java` (~15 行)
-- **配置变更**:`application.yml` 打开 `swagger.enable`(默认仍 false,prod 可覆盖);`management.endpoints.web.exposure.include=health,info`
-- **logback 变更**:pattern 加 `[%X{traceId:-}]`
-- **对业务模块影响**:零破坏,业务不用改一行;重启即可看到效果
-- **URL 路径**:因 `server.servlet.context-path: /api`,文档位于 `/api/swagger-ui.html`,健康检查位于 `/api/actuator/health`

+ 0 - 42
openspec/changes/add-observability-foundation/specs/observability-foundation/spec.md

@@ -1,42 +0,0 @@
-## ADDED Requirements
-
-### Requirement: TraceId 贯穿请求与日志
-
-系统 SHALL 对每个进入基座的 HTTP 请求提取或生成一个 traceId,写入 SLF4J MDC,使 logback 输出的每行日志都带上 `[traceId]` 字段;响应写回 `X-Trace-Id` 头;请求结束时清理 MDC。
-
-#### Scenario: 客户端携带 X-Trace-Id
-- **WHEN** 请求 header 含 `X-Trace-Id: abc123`
-- **THEN** 日志 pattern 中 `%X{traceId}` 输出为 `abc123`;响应头回写同值
-
-#### Scenario: 客户端未携带
-- **WHEN** 请求 header 无 `X-Trace-Id`
-- **THEN** 系统生成 32 位无横线 UUID 作为 traceId;写 MDC + 响应头
-
-#### Scenario: 请求结束清理
-- **WHEN** 请求处理完成(正常或异常)
-- **THEN** MDC 中 traceId 被移除,不会污染下一个线程池复用
-
-### Requirement: OpenAPI 文档可控启用
-
-系统 SHALL 通过 `application.yml` 中 `swagger.enable` 开关控制 OpenAPI/Swagger UI 是否加载;默认 false;启用后 UI 暴露在 `/api/swagger-ui.html`(受 `server.servlet.context-path` 前缀影响)。
-
-#### Scenario: 开发/测试环境启用
-- **WHEN** 在 `application-dev.yml` 中设置 `swagger.enable: true`
-- **THEN** 启动后访问 `<host>/api/swagger-ui.html` 可看到交互式文档
-- **AND** 所有带 Spring MVC 注解(`@RestController` 等)的 controller 自动列出
-
-#### Scenario: 生产关闭
-- **WHEN** `swagger.enable` 为 false 或未配置
-- **THEN** Swagger UI 与 `/v3/api-docs` 均 404;不加载任何 springdoc bean
-
-### Requirement: Actuator 健康检查
-
-系统 SHALL 暴露 `/actuator/health` 与 `/actuator/info` 两个端点,其他 actuator 端点默认关闭。
-
-#### Scenario: 健康检查探针
-- **WHEN** 访问 `<host>/api/actuator/health`
-- **THEN** 返回 200 + `{"status":"UP", ...}`;包含数据源(若配置)健康子项
-
-#### Scenario: 敏感端点默认关闭
-- **WHEN** 访问 `/actuator/env` 或 `/actuator/beans`
-- **THEN** 返回 404(未暴露)

+ 0 - 51
openspec/changes/add-observability-foundation/tasks.md

@@ -1,51 +0,0 @@
-## 1. pom 依赖调整
-
-- [x] 1.1 parent `pom.xml` 的 `<dependencies>` 段增加 `spring-boot-starter-actuator`(由 Spring Boot BOM 托管版本)
-- [x] 1.2 parent `pom.xml` 的 `<dependencies>` 段增加 `org.springdoc:springdoc-openapi-ui:1.6.15`
-- [x] 1.3 parent `pom.xml` 移除孤立的 `<springfox-boot-starter.version>` property
-
-> 备注:mjava/pom.xml 的 `<dependencies>` 为空,实际依赖在 parent pom 集中声明,已改为在 parent pom 增加。
-
-## 2. TraceId 实现
-
-- [x] 2.1 新建 `mjava/src/main/java/com/malk/filter/TraceIdFilter.java`:`OncePerRequestFilter` + MDC 写/清 + 响应头回写
-- [x] 2.2 新建 `mjava/src/main/java/com/malk/core/MdcTaskDecorator.java`:`TaskDecorator` 拷贝 MDC 到子线程
-- [x] 2.3 改造 `AsyncConfig.java`:`getAsyncExecutor()` 和 `aliworkTaskExecutor` 两个线程池都 `setTaskDecorator(new MdcTaskDecorator())`
-- [x] 2.4 `logback-spring.xml` 的 `log.pattern` 在 `[%thread]` 后追加 `[%X{traceId:-}]`
-
-## 3. OpenAPI 配置
-
-- [x] 3.1 新建 `mjava/src/main/java/com/malk/config/OpenApiConfig.java`:`@ConditionalOnProperty(name="swagger.enable", havingValue="true")` 条件加载
-- [x] 3.2 `application.yml` 增加 `springdoc.api-docs.enabled` / `springdoc.swagger-ui.enabled` 与 `swagger.enable` 绑定
-- [x] 3.3 `application-dev.yml` 设 `swagger.enable: true` 让开发环境默认开
-
-## 4. Actuator 配置
-
-- [x] 4.1 `application.yml` 增加 `management.endpoints.web.exposure.include: health,info` + `management.endpoint.health.show-details: when-authorized`
-- [ ] 4.2 ⏳ 启动后手动验证 health 端点(需在 IDE 里启 mjava-guangming 或 mjava-mcli)
-
-## 5. 验证
-
-- [x] 5.1 `openspec validate add-observability-foundation --strict` 通过
-- [ ] 5.2 ⏳ IDE 里 Rebuild Project + 启动冒烟:
-      - 访问 `http://<host>:<port>/api/actuator/health` 返回 `{"status":"UP"}`
-      - 访问 `http://<host>:<port>/api/swagger-ui.html`(dev profile)看到交互文档
-      - 任意请求日志中出现 `[traceId-32位十六进制]` 字段
-      - `curl -H "X-Trace-Id: test-123" ...` 响应头回带 `X-Trace-Id: test-123`
-
-## 冒烟指南
-
-启动后最简验证脚本:
-```bash
-# 1. 健康检查
-curl -i http://localhost:9001/dev/actuator/health
-
-# 2. TraceId 自动生成
-curl -i http://localhost:9001/dev/actuator/info | grep -i "x-trace-id"
-
-# 3. TraceId 透传
-curl -i -H "X-Trace-Id: my-custom-id" http://localhost:9001/dev/actuator/info | grep -i "x-trace-id"
-
-# 4. Swagger UI
-open http://localhost:9001/dev/swagger-ui.html
-```

+ 0 - 2
openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/.openspec.yaml

@@ -1,2 +0,0 @@
-schema: spec-driven
-created: 2026-04-14

+ 0 - 29
openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/design.md

@@ -1,29 +0,0 @@
-## Context
-
-`RSACrypt`(44 行,`encrypt` + `toUrlSafe`)目前只存在 `mjava-guangming/util/` 下。该类没有任何 guangming 业务特定逻辑,纯是 RSA 公钥加密的通用工具。基座已有 `com.malk.server.dingtalk.crypto.DingCallbackCrypto`(钉钉专用,不动),但没有对应的通用加密工具包。
-
-## Goals / Non-Goals
-
-**Goals**
-- `RSACrypt` 在基座可用,未来新业务模块直接 `import com.malk.util.crypto.RSACrypt` 即可
-- 不破坏 `mjava-guangming` 内所有既有调用,业务代码零感知
-
-**Non-Goals**
-- 不加 `decrypt` / `sign` / `verify` 等新方法(无真实需求,避免过度设计)
-- 不加自定义异常类,继续让调用方处理 `Exception`
-- 不改 `DingCallbackCrypto`,它与 RSA 无关
-
-## Decisions
-
-### D1:新建 `com.malk.util.crypto` 包放通用加密工具
-- **理由**:与已有 `com.malk.util.*`(如 `McUtil` 等)的组织一致;未来若需要 AES / SM2 等工具也落在此包
-- **替代**:也可以放 `com.malk.server.dingtalk.crypto/`,拒绝 —— `RSACrypt` 非钉钉专用
-
-### D2:旧类保留为 `@Deprecated` 委托壳,不立即删除
-- **理由**:`mjava-guangming` 内 `MailSsoController` 等调用方已经 import 旧路径。留一个壳能让这些业务代码**本次完全不动**,降低 blast radius
-- **后续**:下次有动到 `mjava-guangming` 的 change 时再顺手把 import 切到新路径,彻底删壳
-
-## Risks / Trade-offs
-
-- **[两份类同时存在的混淆]** → `@Deprecated` + javadoc 指向新类;IDE 使用旧类会有删除线警告,自然引导迁移
-- **[未来 DingCallbackCrypto 是否也该搬]** → 不搬。它绑死钉钉协议,命名本身就带"Ding",留在 `server/dingtalk/crypto/` 语义更准

+ 0 - 30
openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/proposal.md

@@ -1,30 +0,0 @@
-> 状态(2026-04-18 重新索引):**已完成,待归档**
-> tasks.md 全部 [x],仅做了 javac 联合编译验证,未做 mvn 打包回归。若生产 mjava-guangming 正常,应走 `/opsx:archive` 入 `openspec/changes/archive/`。
-
-## Why
-
-`RSACrypt` 加密工具类(44 行)目前仅存在于 `mjava-guangming/src/main/java/com/malk/guangming/util/RSACrypt.java`。若未来有其他业务模块也需要做 RSA 公钥加密(例如对接其他邮箱/SSO 供应商),会出现复制粘贴。应下沉到 `mjava` 基座的通用 util 包,让所有业务共享。
-
-本次不做其他"预防性"抽取:SSO、回调分发等业务模块现有实现都已经是对基座 `DDClient` / `DingCallbackCrypto` 的薄包装,无实际重复,本次不动。
-
-## What Changes
-
-- 新增 `mjava/src/main/java/com/malk/util/crypto/RSACrypt.java`,内容与旧类逐字节一致(`encrypt(str, publicKey)` + `toUrlSafe(base64)`)
-- 旧 `mjava-guangming/.../util/RSACrypt.java` 改为 `@Deprecated` 委托壳,方法内部转调新类(保持二进制/源码兼容)
-
-## Capabilities
-
-### New Capabilities
-- `crypto-utils`: 基座通用加密工具包,首批只放 `RSACrypt`
-
-### Modified Capabilities
-<!-- 无 -->
-
-## Impact
-
-- **代码**:
-  - 新增:`mjava/src/main/java/com/malk/util/crypto/RSACrypt.java`
-  - 修改:`mjava-guangming/src/main/java/com/malk/guangming/util/RSACrypt.java`(退化为委托)
-- **调用方**:`mjava-guangming` 内部既有 `RSACrypt` 调用方(如 `MailSsoController`、`DingTalkAuthService`)无需修改,import 路径保持不变
-- **依赖**:不新增
-- **版本**:`mjava` 保持 `0.0.3`(公有 API 只增不改,未达小版本级别;如后续还要加新能力再统一升版)

+ 0 - 18
openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/specs/crypto-utils/spec.md

@@ -1,18 +0,0 @@
-## ADDED Requirements
-
-### Requirement: RSACrypt 下沉到基座
-
-系统 SHALL 把现有 `com.malk.guangming.util.RSACrypt` 完整复制到基座 `com.malk.util.crypto.RSACrypt`,保持方法签名与行为完全一致(`encrypt(str, publicKey)` + `toUrlSafe(base64)`)。本次**不新增**任何方法。
-
-#### Scenario: 基座新位置可用
-- **WHEN** 任一业务模块 import `com.malk.util.crypto.RSACrypt`
-- **THEN** 调用 `encrypt`、`toUrlSafe` 结果与旧 `com.malk.guangming.util.RSACrypt` 逐字节一致
-
-### Requirement: 旧位置保留 Deprecated 壳
-
-系统 SHALL 在 `com.malk.guangming.util.RSACrypt` 保留类名与方法签名,内部委托到新类并加 `@Deprecated`,避免破坏已编译的业务代码。
-
-#### Scenario: 旧代码继续编译运行
-- **WHEN** 业务代码仍写 `com.malk.guangming.util.RSACrypt.encrypt(...)`
-- **THEN** 编译通过;运行结果与调用新类完全一致
-- **AND** 首次调用打印一次 WARN 日志引导迁移(可选,不阻塞功能)

+ 0 - 12
openspec/changes/archive/2026-04-18-extract-dingtalk-standard-api/tasks.md

@@ -1,12 +0,0 @@
-## 1. 基座新增
-
-- [x] 1.1 新建 `mjava/src/main/java/com/malk/util/crypto/RSACrypt.java`,内容与旧类逐字节一致,仅改 package 声明为 `com.malk.util.crypto`
-
-## 2. 业务模块改造
-
-- [x] 2.1 改写 `mjava-guangming/src/main/java/com/malk/guangming/util/RSACrypt.java` 为 `@Deprecated` 委托壳,两个方法内部转调 `com.malk.util.crypto.RSACrypt` 的同名方法
-
-## 3. 验证
-
-- [x] 3.1 `openspec validate extract-dingtalk-standard-api --strict` 通过
-- [x] 3.2 `javac` 两文件联合编译通过(项目用 IDE 构建,无 mvn;已用 javac 做语法层验证)

+ 0 - 130
openspec/changes/archive/2026-04-19-add-request-auth-replay-guard/design.md

@@ -1,130 +0,0 @@
-## 签名协议(客户端必须遵守)
-
-请求头固定格式:
-
-```
-X-MJ-Key:       {调用方标识,便于审计,非秘密}
-X-MJ-Timestamp: {Unix 毫秒}
-X-MJ-Nonce:     {32 字符随机串}
-X-MJ-Signature: {HMAC-SHA256(sharedSecret, timestamp + "\n" + nonce + "\n" + method + "\n" + path + "\n" + bodyHash)}
-```
-
-其中:
-- `method`:`GET` / `POST` / ...(大写)
-- `path`:不含 host 与 query 的路径(如 `/api/mcli/personnel/sync`)
-- `bodyHash`:`SHA256(requestBody bytes)` 的 hex;GET 请求 body 为空字符串
-
-签名算法固定 HMAC-SHA256,结果以 hex 小写输出。
-
-## 请求生命周期
-
-```
-请求到达
-  ↓
-AuthFilter.doFilter
-  ├── enabled == false?  → 直接 chain.doFilter (默认行为)
-  ├── path 在 exempt-paths?  → 放行
-  ├── handler method 有 @NoAuth?  → 放行(需要 spring MVC 上下文,见下)
-  │
-  ├── 校验 Header 齐全 → 缺字段 401
-  ├── 校验 timestamp 在 ±window 内 → 超窗 401
-  ├── 校验 nonce 未用过 → NonceCache.putIfAbsent → 冲突 401(重放)
-  ├── 计算期望 signature → 比对 → 不匹配 403
-  └── 全部通过 → MDC 记录 authKey → chain.doFilter
-
-afterCompletion
-  ├── 清 MDC
-```
-
-**实现难点**:`AuthFilter` 是 `javax.servlet.Filter`,在 Spring MVC 分发前执行,无法直接拿到 `HandlerMethod` 检查 `@NoAuth`。两种解决方案:
-
-- **方案 A**(推荐):`AuthFilter` 只做 Header 齐全性与时间戳/Nonce 校验;签名校验延后到 `HandlerInterceptor.preHandle`(那时能拿到 HandlerMethod,可读 `@NoAuth`)
-- **方案 B**:`AuthFilter` 一次到位,`@NoAuth` 只支持类级别(通过 `RequestMappingHandlerMapping` 提前解析)
-
-Phase 1 选方案 A(拆分两层),让注解豁免精确到方法。
-
-## Nonce 去重
-
-`NonceCache` 基于 `UtilToken.TimedCache`:
-
-- Key = `nonce:{X-MJ-Nonce}`
-- Value = 任意占位(如 `X-MJ-Key`,便于日志)
-- TTL = `mjava.auth.window + 30s`(窗口 + 冗余 30s,避免边界时间戳造成"刚过期还被接受")
-- 容量上限 `nonce-cache-size`,LRU 淘汰
-
-Nonce 已存在 → 判定重放。
-
-## 豁免策略
-
-三种豁免方式(优先级从上到下):
-
-1. **`enabled: false`**(全局开关):完全不做校验
-2. **`exempt-paths`**(路径匹配):Ant 风格,`/actuator/**` / `/api/*/callback/**`
-3. **`@NoAuth`** 注解:方法级或类级,粒度最细
-
-## 密钥管理
-
-- Phase 1:单一共享密钥 `mjava.auth.secret`,通过环境变量 `AUTH_SECRET` 注入
-- 换密钥:两段式。先同时接受旧 + 新密钥(`secret` + `secret-next`),客户端切完后再移除旧密钥。本 change 不做换密钥能力(YAGNI),需要时再加
-- Phase 2(mjava-com):从宜搭权限表单动态加载 `callerId → secret` 映射,替换 Phase 1 的单密钥
-
-## 错误返回
-
-统一返回 `McR.fail()`,错误码:
-
-| HTTP | code | 原因 |
-|------|------|------|
-| 401 | `AUTH_HEADER_MISSING` | 必备 Header 缺失 |
-| 401 | `AUTH_TIMESTAMP_OUT_OF_WINDOW` | 时间戳超窗 |
-| 401 | `AUTH_NONCE_REPLAYED` | Nonce 已使用(重放) |
-| 403 | `AUTH_SIGNATURE_INVALID` | 签名不匹配 |
-| 503 | `AUTH_CONFIG_MISSING` | 服务端未配置 secret(`enabled: true` 但 `secret` 为空) |
-
-出于安全考虑,错误信息**不暴露**具体环节("Header 缺" 不告诉缺哪个、"签名错" 不告诉期望值),避免给攻击者定位线索。日志侧记录详情。
-
-## 配置项详细
-
-```yaml
-mjava:
-  auth:
-    enabled: false                # 开关
-    secret: ${AUTH_SECRET}        # HMAC 共享密钥
-    secret-next:                  # 灰度切换密钥(可选,本 change 先不实现)
-    window: 300                   # 秒
-    nonce-cache-size: 10000       # 条目上限
-    exempt-paths:
-      - /actuator/**
-      - /api/*/callback/**
-      - /api/*/sso/**             # 部分 SSO 回跳自带签名
-```
-
-## 兼容策略
-
-- 默认 `enabled: false`,生产服务启动后行为 = 当前现状 ✅ 零破坏
-- 客户升级时机:客户侧接入完成签名 → 打开 `enabled: true` → 灰度切换
-- 若某客户永远不开启,也不影响(功能可选)
-
-## Non-Goals(设计边界)
-
-- 不做分布式 Nonce 去重(单实例内存够用;多实例部署时再评估 Redis,见 BACKLOG 触发条件)
-- 不做密钥轮换自动化
-- 不做基于 IP 的访问控制(交给运维层 / 网关)
-- 不做 QPS 限流(`CallerRateLimiter` 是独立专项,见 mjava-com)
-
-## 与 mjava-com 的关系
-
-| 层 | 提供什么 | 谁用 |
-|----|---------|------|
-| 本 change(`request-auth` + `replay-guard`) | 通用 HMAC 签名 + 防重放基础设施 | 所有 mjava 子模块(按需启用) |
-| `add-mjava-com` | 调用方注册表(宜搭权限表单)+ 多 caller 独立密钥 + 限流 | mjava-com 网关 |
-
-mjava-com 的 `CallerAuthInterceptor` 内部将**直接调用本 change 提供的 `UtilSignature` 与 `NonceCache`**,只增加"按 callerId 动态查 secret"与"按 callerId 限流"两层。
-
-## 风险与缓解
-
-| 风险 | 缓解 |
-|------|------|
-| 默认关闭 → 用户忘记开启,裸奔 | README + CLAUDE.md 明示"生产客户建议开启" |
-| 签名计算客户端实现差异大 | 提供 Java / Python / Node.js 三份参考实现代码 |
-| 时间窗过严导致客户端时钟漂移失败 | 默认 5 分钟宽松;文档说明 NTP 同步要求 |
-| `@NoAuth` 滥用 | Code review 规定:豁免必须在 PR 描述里写明理由 |

+ 0 - 64
openspec/changes/archive/2026-04-19-add-request-auth-replay-guard/proposal.md

@@ -1,64 +0,0 @@
-> 状态(2026-04-18 立项):**提案阶段,未实施**
-> 优先级:高(Phase B.2 基座安全增强,早于 mjava-com 专项)
-
-## Why
-
-mjava 基座当前 API 安全裸奔:
-
-- **无统一鉴权**:任何知道 URL 的调用方都能直接访问任意 Controller(`filter/RequestFilter` 和 `RequestInterceptor` 当前只打日志,不做任何校验)
-- **无防重放保护**:即便将来加签名,不带时间窗 + Nonce 的话攻击者仍可抓包重放
-- **客户子项目各自重造**:mjava-guangming 的 SSO 校验、mjava-shunfeng 的 dingtalk webhook 校验各自实现,代码不共享
-
-需要在基座提供**可选启用**的"请求鉴权 + 防重放"统一能力。默认关闭(保护现有 mcli/shunfeng/guangming 生产服务不中断),子项目按需开启;mjava-com 未来的调用方签名也将复用本能力,避免重复建设。
-
-与 `add-observability-foundation` 的 TraceId 互补:前者负责"日志可追溯",本 change 负责"调用不可伪造"。
-
-## What Changes
-
-新增 3 个基座组件(全部放在 `mjava/`,子项目零代码改动即可通过配置启用):
-
-- `com.malk.filter.AuthFilter`:请求进入时校验 token/signature + 时间窗 + Nonce 去重;失败返回 `401/403`
-- `com.malk.utils.UtilSignature`:HMAC-SHA256 签名计算与验证工具类
-- `com.malk.core.NonceCache`:基于 `UtilToken.TimedCache` 的 Nonce 去重缓存(封装 namespace + TTL = 时间窗)
-
-配置开关:
-
-```yaml
-mjava:
-  auth:
-    enabled: false           # 默认关闭;子项目可按需打开
-    secret: ${AUTH_SECRET}   # 服务端共享密钥(环境变量注入)
-    window: 300              # 时间窗秒数,默认 5 分钟
-    nonce-cache-size: 10000  # Nonce 缓存条目上限(LRU 淘汰)
-    exempt-paths:            # 豁免路径清单(正则或 Ant 风格)
-      - /actuator/**         # 健康检查
-      - /api/*/callback/**   # 第三方 webhook 回调(自带签名协议)
-```
-
-另加 `@NoAuth` 注解,方法/类级别显式豁免。
-
-## Capabilities
-
-### New Capabilities
-- `request-auth`:基于 HMAC-SHA256 + 共享密钥的统一请求鉴权
-- `replay-guard`:基于时间窗 + Nonce 的防重放保护
-
-### Modified Capabilities
-<!-- 基座 filter 链追加,不改既有 RequestFilter/RequestInterceptor 职责 -->
-
-## Impact
-
-- **新增文件**:`AuthFilter.java` / `UtilSignature.java` / `NonceCache.java` / `AuthConfig.java` / `@NoAuth` 注解 / `AuthConfigProperties.java`
-- **不动**:`RequestFilter` / `RequestInterceptor` / `CatchException` 原职责
-- **yml**:`application.yml` 新增 `mjava.auth.*` 结构,默认 `enabled: false`
-- **对 mcli / shunfeng / guangming**:零影响(`enabled: false` 默认不拦截)
-- **对 mjava-com**:未来的调用方鉴权直接用本 `UtilSignature` + `NonceCache`;mjava-com 的 `CallerAuthInterceptor` 设计可简化
-
-## Non-Goals
-
-- ❌ 不做 OAuth2 / OpenID Connect(重量级,不适合内部场景)
-- ❌ 不做 JWT 签发与刷新流程(当前是无状态 HMAC,够用;将来如需再单独提案)
-- ❌ 不做 mTLS(网络层配置由运维侧处理)
-- ❌ 不取代第三方 webhook 回调的供应商签名校验(钉钉 `DingCallbackCrypto` / 宜搭 token 验证等各自保留)
-- ❌ 不做角色/权限(RBAC/ABAC),只解决"是谁 + 请求是否合法 + 是否重放"三件事
-- ❌ 本 change 不强制要求任何现存客户升级开启

+ 0 - 58
openspec/changes/archive/2026-04-19-add-request-auth-replay-guard/specs/replay-guard/spec.md

@@ -1,58 +0,0 @@
-## ADDED Requirements
-
-### Requirement: 时间窗校验
-
-mjava 基座 SHALL 拒绝时间戳超出配置窗口的请求,以限制重放攻击的有效窗口。
-
-#### Scenario: 时间戳在窗口内
-
-- **WHEN** `X-MJ-Timestamp` 与服务端当前时间差 ≤ `mjava.auth.window`(默认 300 秒)
-- **THEN** 进入后续 Nonce 与签名校验
-
-#### Scenario: 时间戳超窗
-
-- **WHEN** 时间差 > `window`(无论提前还是滞后)
-- **THEN** 返回 `401 { code: "AUTH_TIMESTAMP_OUT_OF_WINDOW" }`
-- **AND** 审计日志记录 `clientTimestamp` 与 `serverTimestamp` 供排查
-
-### Requirement: Nonce 去重
-
-mjava 基座 SHALL 通过 Nonce 缓存实现在时间窗内的一次性请求保证。同一 Nonce 在窗口期内只允许通过一次。
-
-#### Scenario: 首次 Nonce 被接受
-
-- **WHEN** 请求携带之前未见过的 `X-MJ-Nonce`
-- **THEN** `NonceCache.putIfAbsent(nonce)` 成功
-- **AND** 请求进入签名校验
-
-#### Scenario: Nonce 被重放
-
-- **WHEN** 请求携带之前已见过的 Nonce(仍在 TTL 内)
-- **THEN** `NonceCache.putIfAbsent` 返回 false
-- **AND** 返回 `401 { code: "AUTH_NONCE_REPLAYED" }`
-
-#### Scenario: Nonce 缓存容量上限
-
-- **WHEN** `NonceCache` 达到 `mjava.auth.nonce-cache-size` 上限
-- **THEN** 按 LRU 策略淘汰最旧条目
-- **AND** 淘汰不影响未淘汰 Nonce 的判定正确性
-
-### Requirement: Nonce 缓存 TTL 设置
-
-`NonceCache` 的 TTL MUST 略长于 `mjava.auth.window`,以避免边界时间戳被错误接受。
-
-#### Scenario: TTL 等于窗口 + 30s
-
-- **WHEN** NonceCache 初始化
-- **THEN** TTL = `mjava.auth.window + 30` 秒
-- **AND** 例:window=300 → TTL=330
-
-### Requirement: 单实例内存限制声明
-
-Phase 1 的 Nonce 去重 SHALL 在单 JVM 实例内生效;多实例部署时同一 Nonce 可能被两个实例各接受一次。这是已知限制,不在本 change 范围。
-
-#### Scenario: 多实例部署告警
-
-- **WHEN** 生产部署判定需要多实例横向扩展
-- **THEN** 运维必须评估是否升级为分布式 Nonce(Redis),独立走新 change 提案
-- **AND** 本 change 的 NonceCache 配置 `nonce-cache-size` 建议单实例足够承载高峰 QPS × window 秒的组合

+ 0 - 62
openspec/changes/archive/2026-04-19-add-request-auth-replay-guard/specs/request-auth/spec.md

@@ -1,62 +0,0 @@
-## ADDED Requirements
-
-### Requirement: HMAC 签名校验
-
-mjava 基座 SHALL 提供可配置的 HMAC-SHA256 请求签名校验机制。当 `mjava.auth.enabled=true` 时,所有非豁免请求 MUST 通过签名校验才能进入业务 Controller。
-
-#### Scenario: 合法签名请求通过
-
-- **WHEN** 请求携带完整四个 Header(`X-MJ-Key` / `X-MJ-Timestamp` / `X-MJ-Nonce` / `X-MJ-Signature`)
-- **AND** 签名 = `HMAC-SHA256(secret, timestamp + "\n" + nonce + "\n" + method + "\n" + path + "\n" + bodyHash)`
-- **THEN** 请求被放行到 Controller
-- **AND** MDC 写入 `authKey` 字段供日志追溯
-
-#### Scenario: 签名不匹配
-
-- **WHEN** 计算出的签名与 Header 中 `X-MJ-Signature` 不一致
-- **THEN** 返回 `403 { code: "AUTH_SIGNATURE_INVALID" }`
-- **AND** 响应 message 不包含期望签名值(避免泄露线索)
-- **AND** 审计日志记录 `authKey + path + latencyMs`
-
-#### Scenario: Header 缺失
-
-- **WHEN** 四个必备 Header 中任一缺失
-- **THEN** 返回 `401 { code: "AUTH_HEADER_MISSING" }`
-- **AND** 响应不说明具体缺失哪个
-
-### Requirement: 豁免机制
-
-本能力 SHALL 支持三级豁免策略,优先级从上到下:全局开关 > 路径豁免 > 注解豁免。
-
-#### Scenario: 全局关闭
-
-- **WHEN** `mjava.auth.enabled=false`
-- **THEN** 所有请求跳过鉴权,行为等同于 enable 前的裸奔状态
-
-#### Scenario: 路径豁免
-
-- **WHEN** 请求路径匹配 `mjava.auth.exempt-paths` 列表中任一 Ant 风格模式
-- **THEN** 跳过签名校验
-- **AND** 典型清单:`/actuator/**`(健康检查)、`/api/*/callback/**`(第三方 webhook)
-
-#### Scenario: 注解豁免
-
-- **WHEN** 目标 Controller 方法或其类带 `@NoAuth` 注解
-- **THEN** 签名校验跳过
-- **AND** 注解识别发生在 `HandlerInterceptor.preHandle`(能访问到 `HandlerMethod`)
-
-### Requirement: 密钥配置
-
-`secret` MUST 通过环境变量注入,禁止硬编码。`enabled=true` 但 `secret` 为空时 MUST 拒绝启动或拒绝请求。
-
-#### Scenario: secret 未配置
-
-- **WHEN** `mjava.auth.enabled=true` 但 `mjava.auth.secret` 为空或未设置
-- **THEN** 启动时打 ERROR 日志
-- **AND** 所有受保护请求返回 `503 { code: "AUTH_CONFIG_MISSING" }`
-
-#### Scenario: secret 从环境变量读取
-
-- **WHEN** `application.yml` 中 `secret: ${AUTH_SECRET}`
-- **AND** 部署环境设置 `AUTH_SECRET` 变量
-- **THEN** 启动时成功加载,后续请求按此密钥校验

+ 0 - 59
openspec/changes/archive/2026-04-19-add-request-auth-replay-guard/tasks.md

@@ -1,59 +0,0 @@
-## 1. 签名工具与 Nonce 缓存
-
-- [x] 1.1 新建 `UtilSignature.java`:sha256Hex + sign(HMAC-SHA256) + safeEquals(常量时间比较)
-- [x] 1.2 新建 `NonceCache.java`:基于 Hutool TimedCache,TTL=window+30s,synchronized putIfAbsent
-- [ ] 1.3 单元测试:`UtilSignatureTest`(阻塞 Maven 未装)
-- [ ] 1.4 单元测试:`NonceCacheTest`(阻塞 Maven 未装)
-
-## 2. 配置与注解
-
-- [x] 2.1 新建 `AuthConfigProperties`(prefix mjava.auth,默认值齐全)
-- [ ] 2.2 ~~独立 AuthConfig~~:不需要,AuthConfigProperties @Component 足够
-- [x] 2.3 新建 `NoAuth` 注解(METHOD + TYPE)
-- [x] 2.4 `application.yml` 新增 mjava.auth.* 段,默认 enabled=false + ${AUTH_SECRET} 占位
-- [ ] 2.5 子模块 application-*.yml.example(延后,启用时同步)
-
-## 3. Filter + Interceptor 两层
-
-- [x] 3.1 `AuthFilter`:@Order(HIGHEST+20) 位于 TraceIdFilter 后;enabled/exempt 放行;Header + 时间戳窗口 + Nonce
-- [x] 3.2 靠 @Component + @Order 自动注册到 Spring Boot Filter 链
-- [x] 3.3 `AuthInterceptor`:preHandle 识别 @NoAuth + 读 body + 签名校验 + safeEquals
-- [x] 3.4 WebConfiguration 注册 AuthInterceptor
-- [x] 3.5 失败返回 `McR.error(code, message)` + 对应 HTTP 状态
-
-## 4. 审计日志集成
-
-- [x] 4.1 AuthFilter / AuthInterceptor 失败分支 log.warn(point logger)
-- [x] 4.2 成功路径写 MDC authKey,finally 清理
-- [ ] 4.3 logback pattern 追加 `[%X{authKey:-}]`(延后)
-
-## 5. 参考客户端实现(文档侧)
-
-- [ ] 5.1 Java 客户端签名示例(工具类形式,便于其他 Java 系统 copy)
-- [ ] 5.2 Python 客户端签名示例(`requests` + `hmac`)
-- [ ] 5.3 Node.js 客户端签名示例(`node:crypto`)
-- [ ] 5.4 curl + openssl 命令行示例(方便临时调试)
-
-## 6. 回归测试
-
-- [ ] 6.1 `AuthFilterTest`:enabled/disabled 切换、Header 缺失、时间窗边界、Nonce 重放
-- [ ] 6.2 `AuthInterceptorTest`:`@NoAuth` 方法级/类级、签名校验、exempt-paths
-- [ ] 6.3 集成测试:mjava-mcli 开启后用签名客户端调一次 → 成功;改签名 → 403;改时间戳 → 401
-- [ ] 6.4 mcli/shunfeng/guangming **保持 `enabled: false`** 场景回归(零破坏验证)
-
-## 7. 文档
-
-- [ ] 7.1 `/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md` 新增 §13(或合并到 §6)"请求鉴权与防重放"章节,含签名协议与开关
-- [ ] 7.2 README.md(本 change 完成后):客户端签名接入指引
-- [ ] 7.3 BACKLOG.md 更新专项状态
-
-## 8. 验证
-
-- [ ] 8.1 `openspec validate add-request-auth-replay-guard --strict` 通过
-- [ ] 8.2 开启 enabled 后用 `curl + openssl` 跑通一次真实请求
-- [ ] 8.3 Nonce 重放测试:同签名连发两次 → 第二次 401
-
-## 9. 交付
-
-- [ ] 9.1 PR 附签名协议示意图
-- [ ] 9.2 走 `/opsx:archive`

+ 0 - 119
openspec/changes/archive/2026-04-19-extend-dingtalk-contacts-api/design.md

@@ -1,119 +0,0 @@
-## 目标结构
-
-`DDClient_Contacts` 内部按子模块分段,同一文件内保持阅读连贯性(不拆成 `_Contacts_User` / `_Contacts_Dept`):
-
-```java
-public interface DDClient_Contacts {
-    // ========== 用户管理 User ==========
-    Map createUser(...);          // 旧, 保留
-    Map createUser_v2(...);       // 新对齐
-    Map updateUser(...);          // 新增(旧无此方法)
-    ...
-
-    // ========== 部门管理 Department ==========
-    Map createDepartment(...);
-    Map createDepartment_v2(...);
-    ...
-
-    // ========== 角色管理 Role ==========
-    Map addRole(...);             // 新增
-    Map listRoles(...);
-    ...
-
-    // ========== 员工字段管理 EmployeeField ==========
-    Map hideEmployeeField(...);
-    ...
-}
-```
-
-## v2 命名约定
-
-同一个 endpoint 若**旧方法签名不规范**(参数缺漏、命名不统一),新方法用 `_v2` 后缀:
-
-- `createUser` (旧) 不删
-- `createUser_v2` (新) 按 §3.4.2 规范,所有 body_ext 字段 javadoc 列全
-
-若**旧方法不存在**(真正新增),直接用正常名字 `updateUser`,无需 `_v2`。
-
-## 覆盖度矩阵(任务 0 填写模板)
-
-| 官方 endpoint | 当前方法 | 对齐行动 |
-|--------------|---------|---------|
-| POST /topapi/v2/user/create | `createUser` | 新增 `createUser_v2` |
-| POST /topapi/v2/user/update | ❌ 缺 | 新增 `updateUser` |
-| POST /topapi/v2/user/delete | `deleteUser` | 不动(签名已规范) |
-| POST /topapi/v2/user/get | `getUserInfoById` | 新增 `getUser_v2`(命名统一) |
-| POST /topapi/v2/user/getbymobile | `getUserInfoByMobile` | 新增 `getUserByMobile_v2` |
-| POST /topapi/user/listsimple | ❌ 缺 | 新增 `listUsersSimple` |
-| POST /topapi/v2/user/list | 部分(`listSubDepartmentDetail`?)| 新增 `listDeptUserDetail_v2` |
-| GET /topapi/user/listid | `listDepartmentUserId` | 不动 |
-| POST /topapi/inactive/user/v2/get | ❌ 缺 | 新增 `listInactiveUsers` |
-| POST /topapi/user/getbyunionid | ❌ 缺 | 新增 `getUserByUnionId` |
-| POST /topapi/user/get_admin | ❌ 缺 | 新增 `listAdmins` |
-| POST /topapi/smartwork/hrm/employee/listdimission | `getLeaveEmployeeRecords` | 新增 `listDimissionEmployees_v2` |
-| POST /topapi/v2/department/create | `createDepartment` | 新增 `createDepartment_v2` |
-| POST /topapi/v2/department/update | ❌ 缺 | 新增 `updateDepartment` |
-| POST /topapi/v2/department/delete | ❌ 缺 | 新增 `deleteDepartment` |
-| POST /topapi/v2/department/get | `getDepartmentInfo` | 新增 `getDepartment_v2` |
-| POST /topapi/v2/department/listsubid | `listSubDepartmentId` | 不动 |
-| POST /topapi/v2/department/listparentbyuser | `listParentByUser` | 不动 |
-| POST /topapi/v2/department/listparentbydept | ❌ 缺 | 新增 `listParentByDept` |
-| POST /topapi/v2/department/listsub | `listSubDepartmentDetail` | 新增 `listSubDepartments_v2` |
-| POST /topapi/v2/role/add_role | ❌ 缺 | 新增 `addRole` |
-| POST /topapi/v2/role/addrolesforemps | ❌ 缺 | 新增 `addRolesForEmps` |
-| POST /topapi/v2/role/removerolesforemps | ❌ 缺 | 新增 `removeRolesForEmps` |
-| POST /topapi/v2/role/update_role | ❌ 缺 | 新增 `updateRole` |
-| POST /topapi/v2/role/list | ❌ 缺 | 新增 `listRoles` |
-| POST /topapi/v2/role/getrole | ❌ 缺 | 新增 `getRole` |
-| POST /topapi/v2/role/simplelist | ❌ 缺 | 新增 `listRoleEmployees` |
-| POST /topapi/hide_field/add_or_update | ❌ 缺 | 新增 `upsertHideField` |
-| POST /topapi/hide_field/remove | ❌ 缺 | 新增 `removeHideField` |
-| POST /topapi/hide_field/query | ❌ 缺 | 新增 `listHideFields` |
-
-总计:**~30 个方法**,其中 **~17 个新增**,**~8 个 v2 对齐**,**~5 个保持不动**。
-
-## 方法签名模板
-
-```java
-/**
- * 创建用户(v2 对齐版)
- * @apiNote https://open.dingtalk.com/document/orgapp/user-information-creation
- *
- * @param access_token  钉钉 accessToken(非企业内部应用按文档约定)
- * @param name          员工姓名(必填)
- * @param mobile        手机号(必填,同一企业内不重复)
- * @param dept_id_list  所属部门 ID 列表(必填,至少 1 个)
- * @param body_ext 可选参数:
- *   - userid (String): 自定义 userid,不传则自动生成
- *   - hired_date (Long): 入职时间毫秒时间戳
- *   - job_number (String): 工号
- *   - title (String): 职位
- *   - work_place (String): 办公地点
- *   - remark (String): 备注
- *   - email (String): 邮箱(应用需有通讯录读写权限)
- *   - org_email (String): 企业邮箱
- *   - org_email_type (String): profession / premium
- *   - extension (String): 扩展属性 JSON 字符串
- *   - senior_mode (Boolean): 是否高管模式
- *   - hide_mobile (Boolean): 是否隐藏手机号
- *   - telephone (String): 分机号
- *   - dept_order_list (List<Map>): 部门内排序
- *   - dept_title_list (List<Map>): 部门内职位
- *   - login_email (String): 企业账号登录邮箱
- *   - exclusive_account (Boolean): 是否专属账号
- *   - exclusive_account_type (String): sso/custom
- *   - exclusive_mobile (String): 专属账号手机号
- *   - ...(完整清单见官方文档,最后同步日期 2026-04-18)
- * @return Map 包含 userid、isLeaderInDepts 等
- */
-Map createUser_v2(String access_token, String name, String mobile,
-                  List<String> dept_id_list, Map<String, Object> body_ext);
-```
-
-## 风险与缓解
-
-| 风险 | 缓解 |
-|------|------|
-| body_ext javadoc 随官方文档变动脱节 | javadoc 末尾强制写"最后同步日期",下次对齐时更新 |
-| 新旧方法并存造成调用方选择困惑 | README 加明确指引"新项目用 _v2 / 无后缀版本;历史项目原地不动" |
-| 新增方法数量大(~17 个)导致 PR 过重 | 按用户 / 部门 / 角色 / 员工字段四个子模块拆 4 个 PR |

+ 0 - 44
openspec/changes/archive/2026-04-19-extend-dingtalk-contacts-api/proposal.md

@@ -1,44 +0,0 @@
-> 状态(2026-04-18 立项):**规则定义阶段,不做代码改造**
-> 优先级:高(Phase B.1 第二个专项)
-> 改造等宜搭专项规则走通后启动
-
-## Why
-
-`DDClient_Contacts` 当前有 ~18 个方法,对齐钉钉通讯录的部分 endpoint,但:
-
-- **参数不完整**:`createUser(access_token, name, mobile, dept_id_list, body_ext)` 只显式暴露了 3 个必填参数,其他如 `userid` / `hired_date` / `job_number` / `email` / `title` 全塞在 `body_ext` 里,**javadoc 未枚举**,调用方要查官方文档才知道能传什么。
-- **缺失 endpoint**:钉钉通讯录目前官方提供 30+ 个 endpoint,现有实现只覆盖 18 个,缺 `updateUser` / `listsimpleUser` / `getUserByUnionId` / `getAdminList` / `listInactive` / `createDepartment_v2` / `updateDepartment` / `listParentByDept` / 角色管理(addRole/delRole/listRoles/...)/ 员工字段隐藏设置 等。
-- **命名不一致**:`getUserInfoById` vs `getDepartmentInfo` 动词选择混乱,新方法应统一 `get{Resource}` / `list{Resource}` / `create{Resource}` 三模式。
-
-## What Changes
-
-**保留 `DDClient_Contacts` 现有方法**(已上线客户依赖),按 `mjava-baseline §3.4.4` **新增对齐方法**:
-
-- 补齐缺失 endpoint(参照钉钉官方通讯录 API 列表)
-- 对已有方法做**签名对齐新版本**(方法名加后缀 `_v2` 或重载),`body_ext` 字段在 javadoc 完整枚举
-- 按"用户管理 / 部门管理 / 角色管理 / 员工字段管理"四个子模块在同一接口内分段组织
-- 后续专项统一把旧方法标 `@Deprecated`(不在本 change 做)
-
-涉及范围**仅钉钉通讯录**;考勤 / OA 审批 / 消息 / 群聊等其他 DDClient_{Domain} 模块不在此次提案。
-
-## Capabilities
-
-### New Capabilities
-- `dingtalk-contacts-v2`:对齐后的通讯录原子接口集(含用户 / 部门 / 角色 / 员工字段)
-
-### Modified Capabilities
-- `dingtalk-contacts-legacy`(现有):标记为后续弃用,保留原有接口
-
-## Impact
-
-- **修改文件**:`DDClient_Contacts.java` 追加方法;`impl/DDImplClient_Contacts.java` 追加实现
-- **不改动**:旧方法签名
-- **API 消费者**:旧方法继续可用;新方法可按需切换
-- **三个生产客户**:零影响
-
-## Non-Goals
-
-- ❌ 不删或改旧方法
-- ❌ 不做考勤 / OA 审批 / 消息 / 群聊等模块(每模块独立 change)
-- ❌ 不做企业外部联系人、专属账号的完整覆盖(现有 `DDClient_Dedicated` 已部分覆盖,按需再评估)
-- ❌ 不做新旧 API 自动路由(由调用方显式选择 v2 方法)

+ 0 - 95
openspec/changes/archive/2026-04-19-extend-dingtalk-contacts-api/specs/dingtalk-contacts-v2/spec.md

@@ -1,95 +0,0 @@
-## ADDED Requirements
-
-### Requirement: 用户管理完整覆盖
-
-`DDClient_Contacts` SHALL 提供对钉钉官方通讯录用户管理 endpoint 的完整对齐。所有方法 MUST 遵守 `mjava-baseline §3.4.2` 的签名规则。
-
-#### Scenario: 创建用户(完整字段)
-
-- **WHEN** 调用 `createUser_v2(access_token, name, mobile, dept_id_list, body_ext)`
-- **THEN** `body_ext` 必须支持官方文档列出的所有可选字段(userid / hired_date / job_number / title / email / senior_mode / extension / ...)
-- **AND** javadoc 必须枚举这些字段并注明类型
-
-#### Scenario: 更新用户
-
-- **WHEN** 调用 `updateUser(access_token, userid, body_ext)`
-- **THEN** `userid` 必填,其他字段全部通过 `body_ext` 传入
-- **AND** 不传的字段保持钉钉侧原值不变
-
-#### Scenario: 按 unionId 查询
-
-- **WHEN** 调用 `getUserByUnionId(access_token, union_id)`
-- **THEN** 返回 userid(二次调用 `getUser_v2` 获取详情)
-
-#### Scenario: 查询管理员
-
-- **WHEN** 调用 `listAdmins(access_token)`
-- **THEN** 返回管理员 userid 列表,含主管理员与子管理员区分字段
-
-#### Scenario: 查询未激活员工
-
-- **WHEN** 调用 `listInactiveUsers(access_token, is_active, offset, size, body_ext)`
-- **THEN** 支持按激活状态过滤
-
-### Requirement: 部门管理完整覆盖
-
-`DDClient_Contacts` SHALL 提供部门 CRUD 与层级查询的完整对齐。所有方法 MUST 对应钉钉官方部门管理 endpoint。
-
-#### Scenario: 部门 CRUD
-
-- **WHEN** 依次调用 `createDepartment_v2` / `updateDepartment` / `deleteDepartment` / `getDepartment_v2`
-- **THEN** 每个方法参数完整对齐官方文档
-- **AND** `body_ext` 支持 `hide_dept` / `dept_permits` / `user_permits` / `outer_dept` / `source_identifier` 等全部可选项
-
-#### Scenario: 父部门链查询
-
-- **WHEN** 调用 `listParentByDept(access_token, dept_id)`
-- **THEN** 返回目标部门到根部门的父链
-
-### Requirement: 角色管理
-
-`DDClient_Contacts` SHALL 提供角色的增删改查与批量员工授权能力。
-
-#### Scenario: 新增角色
-
-- **WHEN** 调用 `addRole(access_token, roleName, groupId)`
-- **THEN** 返回 roleId
-
-#### Scenario: 批量分配角色
-
-- **WHEN** 调用 `addRolesForEmps(access_token, roleIds, userIds)`
-- **THEN** 一次调用同时为多员工授权多角色
-
-#### Scenario: 查询角色成员
-
-- **WHEN** 调用 `listRoleEmployees(access_token, role_id, size, offset)`
-- **THEN** 分页返回角色下员工列表
-
-### Requirement: 员工字段可见性
-
-`DDClient_Contacts` SHALL 提供员工档案字段隐藏规则的管理能力。
-
-#### Scenario: 隐藏字段设置
-
-- **WHEN** 调用 `upsertHideField(access_token, name, field, userIds, deptIds)`
-- **THEN** 对指定员工或部门隐藏指定员工档案字段
-
-#### Scenario: 查询隐藏字段
-
-- **WHEN** 调用 `listHideFields(access_token, size, offset)`
-- **THEN** 返回企业当前生效的字段隐藏规则列表
-
-### Requirement: 参数透传约束
-
-本 capability 所有方法 MUST 满足官方可选参数完整透传与文档化要求,不得因 Java 侧建模省事而过滤字段。
-
-#### Scenario: body_ext 不得过滤官方可选参数
-
-- **WHEN** 调用方通过 `body_ext` 传入官方文档支持的任意可选字段
-- **THEN** 实现必须原样透传到 HTTP 请求体,不得删除或改写
-- **AND** 方法 javadoc 必须列出所有已知 body_ext key 并注明类型
-
-#### Scenario: 方法必须有 apiNote
-
-- **WHEN** 新增任一 Client 方法
-- **THEN** javadoc 必须含 `@apiNote` 链到该 endpoint 的钉钉官方文档页

+ 0 - 71
openspec/changes/archive/2026-04-19-extend-dingtalk-contacts-api/tasks.md

@@ -1,71 +0,0 @@
-## 0. 覆盖度矩阵(前置,开发者必须对照官方文档最新版填写)
-
-> 见 `design.md` 表格;当前盘点截至 2026-04-18,约 17 项新增 + 8 项 v2 对齐。
-
-## 1. 前置
-
-- [ ] 1.1 ⚠️ **阻塞**:官方文档路径已基于训练知识 + 现有实现推断,实施冒烟前需逐条对照官方文档复核,尤其 Role 模块的 `add_role` / `deleterole` 等路径大小写与下划线格式
-- [x] 1.2 `DDConf` 现有 corpId/appKey/appSecret 字段足够;本次未扩展
-
-## 2. 用户管理(User)
-
-- [x] 2.1 `createUser_v2`(完整 body_ext javadoc 20+ 字段)
-- [x] 2.2 `updateUser`
-- [x] 2.3 `getUser_v2`
-- [x] 2.4 `getUserByMobile_v2`
-- [x] 2.5 `listUsersSimple`
-- [x] 2.6 `listDeptUserDetail_v2`
-- [x] 2.7 `listInactiveUsers`
-- [x] 2.8 `getUserByUnionId`
-- [x] 2.9 `listAdmins`
-- [x] 2.10 `listDimissionEmployees_v2`
-
-## 3. 部门管理(Department)
-
-- [x] 3.1 `createDepartment_v2`
-- [x] 3.2 `updateDepartment`
-- [x] 3.3 `deleteDepartment`
-- [x] 3.4 `getDepartment_v2`
-- [x] 3.5 `listSubDepartments_v2`
-- [x] 3.6 `listParentByDept`
-
-## 4. 角色管理(Role)
-
-- [x] 4.1 `addRole`
-- [x] 4.2 `updateRole`
-- [x] 4.3 `deleteRole`
-- [x] 4.4 `listRoles`
-- [x] 4.5 `getRole`
-- [x] 4.6 `listRoleEmployees`
-- [x] 4.7 `addRolesForEmps`
-- [x] 4.8 `removeRolesForEmps`
-
-## 5. 员工字段管理(EmployeeField)
-
-- [x] 5.1 `upsertHideField`
-- [x] 5.2 `removeHideField`
-- [x] 5.3 `listHideFields`
-
-## 6. 实现
-
-- [x] 6.1 `DDClient_Contacts.java` 接口追加 27 个方法声明(User/Dept/Role/Field 四段分隔清晰)
-- [x] 6.2 `impl/DDImplClient_Contacts.java` 实现全部 27 个方法,复用 `qs()` / `merge()` 模板
-- [x] 6.3 全部走 `DDR.doPost(oapi, null, qs(token), body)` 旧 OAPI 模式(与现有方法一致,禁 SDK)
-- [x] 6.4 每个方法 javadoc 含 `@apiNote` 路径 + `body_ext` 枚举(User 模块 20+ 字段详细)
-
-## 7. 文档
-
-- [ ] 7.1 在 mjava-baseline.md §3.4 表格中追加"dingtalk-contacts-v2 已对齐"状态
-- [ ] 7.2 PR 描述含官方文档对照矩阵截图
-
-## 8. 验证
-
-- [ ] 8.1 单元测试:每个方法的参数透传(mock UtilHttp 验证 body 完整)
-- [ ] 8.2 集成测试:选 5 个关键 endpoint(createUser_v2 / updateUser / listAdmins / addRole / upsertHideField)跑真实钉钉
-- [ ] 8.3 `openspec validate extend-dingtalk-contacts-api --strict` 通过
-- [ ] 8.4 审视 mjava-guangming / mjava-shunfeng / mjava-mcli 是否需要迁移到新 v2 方法(本次**不强制迁**,只评估)
-
-## 9. 交付
-
-- [ ] 9.1 PR 按子模块拆分(User / Dept / Role / Field 四个)
-- [ ] 9.2 走 `/opsx:archive` 归档

+ 0 - 92
openspec/changes/archive/2026-04-19-extend-yida-api-coverage/design.md

@@ -1,92 +0,0 @@
-## 目标架构
-
-```
-┌─────────────────────────────────────────────────────┐
-│ 业务 Controller (mcli / shunfeng / guangming / pro) │
-└─────────────────────┬───────────────────────────────┘
-                      │
-          ┌───────────▼────────────┐
-          │  YDService (组合)      │  公开签名不变
-          └───────────┬────────────┘
-                      │ 内部改调原子
-     ┌────────────────┼────────────────┐
-     │                │                │
-┌────▼────┐    ┌──────▼──────┐   ┌─────▼──────┐
-│YDClient │    │YDClient_Form│   │YDClient_Proc│
-│(deprecated)│  │ (新增原子)  │   │ (新增原子) │
-└─────────┘    └─────────────┘   └─────────────┘
-                      │                │
-                      └────┬───────────┘
-                           ▼
-                      UtilHttp (审计日志 §3.5)
-                      UtilToken (tenantId:yida:appType)
-```
-
-## 模块拆分映射
-
-### YDClient_Form(表单数据)
-
-按宜搭官方文档模块映射:
-
-| 子模块 | 方法前缀 | 备注 |
-|--------|---------|------|
-| 表单实例 CRUD | `saveForm` / `updateForm` / `deleteForm` / `getForm` | 单实例 |
-| 查询 | `searchForm` / `listFormIds` / `listFormsAll` | 分页 / ID 列表 / 全量含子表 |
-| 批量 | `batchSaveForm` / `batchUpsertForm` | 批量接口 |
-| 组件值 | `listComponentValues` | 指定字段取值 |
-| 操作日志 | `listFormOperations` | 审计日志 |
-| 按条件删除 | `deleteFormByCondition` | 条件批量删 |
-
-### YDClient_Process(流程审批)
-
-| 子模块 | 方法前缀 | 备注 |
-|--------|---------|------|
-| 流程实例 | `startProcess` / `terminateProcess` / `revokeProcess` / `getProcess` / `searchProcesses` | 发起 / 终止 / 撤回 / 查 |
-| 任务 | `agreeTask` / `disagreeTask` / `redirectTask` / `ccTask` / `commentTask` / `searchTasks` | 审批动作 |
-| 节点 | `redirectProcess` / `removeNode` | 跳转节点 |
-
-## 方法签名模板(强约束)
-
-```java
-/**
- * 新增表单实例
- * @apiNote https://open.dingtalk.com/document/orgapp/add-or-update-form-instances
- *
- * @param conf            宜搭应用鉴权(appType + systemToken + userId)
- * @param formUuid        表单 UUID(必填)
- * @param formDataJson    表单数据 JSON 字符串(必填,字段名使用表单 fieldId)
- * @param body_ext 可选字段:
- *   - noExecuteExpression (Boolean): 是否不执行公式字段计算
- *   - language (String): 语言(zh_CN/en_US)
- *   - ...(完整清单见官方文档)
- * @return formInstanceId 新增成功的表单实例 ID
- */
-String saveForm(YDConf conf, String formUuid, String formDataJson, Map<String, Object> body_ext);
-```
-
-**强约束**:
-1. 第 1 参永远是 `YDConf`(封装 appType / systemToken / userId)
-2. 必填参数严格按官方文档顺序
-3. 可选参数 `body_ext` 即使当前业务用不到也必须保留
-4. javadoc 必须有 `@apiNote` 官方文档链接
-5. javadoc 必须列出 body_ext 支持的 key(每个 key 标明类型)
-
-## 兼容策略
-
-- `YDClient` 接口文件不动,旧方法签名保持
-- 旧方法在下个 `remove-yida-legacy-aggregate` change 里统一加 `@Deprecated` 并标注迁移目标
-- `YDService` 可以内部悄悄切换到新方法,**只要公开签名不变**
-
-## Non-Goals
-
-- 不重写 `YDParam` 建造者的契约
-- 不做"连接器应用"、"附件管理"、"表单设计器" 模块(作为后续 change 扩展)
-- 不做反射式调度(每个方法显式实现)
-
-## 风险
-
-| 风险 | 缓解 |
-|------|------|
-| 旧 operateData 某些边角 case 在新方法里漏 | 迁移前保留旧路径,新方法就绪后做等价测试再切 YDService |
-| 字段过多,`body_ext` javadoc 难维护 | 文档化**日期与当前官方版本**,后续按需更新;鼓励 PR review 时对照 |
-| 客户子项目里有人绕过 YDService 直接调 YDClient 旧方法 | grep 审计 + 后续 Deprecated + 编译告警推动 |

+ 0 - 55
openspec/changes/archive/2026-04-19-extend-yida-api-coverage/proposal.md

@@ -1,55 +0,0 @@
-> 状态(2026-04-18 立项):**规则定义阶段,不做代码改造**
-> 优先级:**最高**(Phase B.1 首个专项)
-> 改造执行等规则通过后,走 `/opsx:apply extend-yida-api-coverage`
-
-## Why
-
-`YDClient` 当前只有两个聚合方法 `operateData(param, FORM_OPERATION)` 与 `queryData(param, FORM_QUERY)`,通过枚举内部路由到不同宜搭 API。**违反** `mjava-baseline §3.4` 的"Client 原子接口 1:1 对应官方 endpoint"规范:
-
-- 一个方法承载多种语义,调用方必须理解 `FORM_OPERATION` 枚举才能用;
-- 参数通过 `YDParam` 建造者承载,**无法从方法签名直接看出哪些字段必填**;
-- 官方新增 endpoint 时没有明确的落地位置,容易漏或错位;
-- Service 层无法对单一 endpoint 做精准缓存或审计(审计颗粒度只能到 `operateData`)。
-
-现有三个客户模块(mcli / shunfeng / guangming)依赖旧 `operateData` / `queryData`,**不能直接删**。采用"新增对齐方法 + 保留旧方法 @Deprecated"的并行策略(mjava-baseline §3.4.4)。
-
-## What Changes
-
-按宜搭开放平台官方文档,把 `YDClient` 拆分为:
-
-```
-service/aliwork/
-├── YDClient.java              # 保留旧 operateData/queryData, 标 @Deprecated
-├── YDClient_Form.java         # 表单数据 API 原子接口(新增)
-├── YDClient_Process.java      # 流程审批 API 原子接口(新增)
-└── YDService.java             # 组合服务(保留,内部改调 Form/Process 原子方法)
-```
-
-- 新增 `YDClient_Form`:覆盖表单实例增删改查、批量、按条件删除、查询组件值等
-- 新增 `YDClient_Process`:覆盖发起/同意/拒绝/转交/跳转/撤回/终止/任务评论等
-- 方法签名严格按 `mjava-baseline §3.4.2` 规则:必填参数显式、可选参数放 `Map body_ext` 并在 javadoc 中**穷举**
-- 每个方法 `@apiNote` 链到宜搭官方文档对应页面
-
-## Capabilities
-
-### New Capabilities
-- `yida-form-atomic`:宜搭表单原子接口集
-- `yida-process-atomic`:宜搭流程原子接口集
-
-### Modified Capabilities
-- `yida-legacy-aggregate`(现有):标记为 deprecated,保持现有行为
-
-## Impact
-
-- **新增文件**:`YDClient_Form.java` + `YDClient_Process.java` + `impl/` 两份实现 + 可能的 `YDParam` 补字段
-- **不动**:`YDClient.operateData` / `queryData` 方法签名与行为
-- **YDService 改造**(可选):内部调用从 `YDClient.operateData` 迁到新原子方法,保持公开签名不变,让调用方透明受益
-- **mcli / shunfeng / guangming 三个客户**:零影响
-- **审计日志**:每个原子方法都能被 `UtilHttp` 单独打点,颗粒度从"操作/查询"提升到具体 endpoint
-
-## Non-Goals
-
-- ❌ 不删旧 `operateData` / `queryData`(等所有新方法稳定 + 全部调用方迁移完成,再单独走一个 `remove-yida-legacy-aggregate` change)
-- ❌ 不封装 SDK 层(依然走 `UtilHttp.doRequest`)
-- ❌ 本提案不做"连接器"、"附件"、"流程设计器"模块(作为后续扩展 change)
-- ❌ 不改 `YDParam` 建造者的对外契约(内部扩字段 OK)

+ 0 - 68
openspec/changes/archive/2026-04-19-extend-yida-api-coverage/specs/yida-form-atomic/spec.md

@@ -1,68 +0,0 @@
-## ADDED Requirements
-
-### Requirement: 表单实例 CRUD 原子接口
-
-`YDClient_Form` SHALL 提供与宜搭官方 1:1 对应的表单实例增删改查原子方法。方法签名 MUST 严格遵守 `mjava-baseline §3.4.2`(必填参数显式 + `body_ext` 承接可选参数 + javadoc 枚举全部 body_ext key + `@apiNote` 链到官方文档)。
-
-#### Scenario: 新增表单实例
-
-- **WHEN** 调用 `YDClient_Form.saveForm(conf, formUuid, formDataJson, body_ext)`
-- **THEN** 发起 `POST /v1.0/yida/forms/instances`(或旧版 `/dingtalk/yida/processes/saveFormData`)
-- **AND** `body_ext` 所有可选参数必须透传(不删、不填默认)
-- **AND** 失败时抛 `McException` 带宜搭原始错误码
-
-#### Scenario: 更新表单实例
-
-- **WHEN** 调用 `YDClient_Form.updateForm(conf, formInstanceId, updateFormDataJson, body_ext)`
-- **THEN** 必须支持 `useLatestVersion` / `ignoreEmpty` 选项(通过 body_ext 透传)
-- **AND** 默认**不改**这两个参数的宜搭侧默认行为
-
-#### Scenario: 删除表单实例
-
-- **WHEN** 调用 `YDClient_Form.deleteForm(conf, formInstanceId, body_ext)`
-- **THEN** 单条删除走 `DELETE` 接口;批量条件删除走 `deleteFormByCondition`
-
-### Requirement: 表单查询原子接口
-
-`YDClient_Form` SHALL 提供分页、ID 列表、全量含子表三种查询粒度,查询方法 MUST 分别对应宜搭官方的不同 endpoint(不混用一个方法承担多种语义)。
-
-#### Scenario: 分页查询
-
-- **WHEN** `searchForm(conf, formUuid, searchFieldJson, currentPage, pageSize, body_ext)`
-- **THEN** 对应 `POST /v1.0/yida/forms/instances/search`
-- **AND** `pageSize` 超过 100 时抛 `McException`(宜搭侧强制上限)
-
-#### Scenario: 全量查询含子表
-
-- **WHEN** `listFormsAll(conf, formUuid, currentPage, pageSize, body_ext)`
-- **THEN** 对应 `retrieve_list_all`(含子表数据),与 `searchForm`(不含子表)行为区分清楚
-
-#### Scenario: 查询组件值
-
-- **WHEN** `listComponentValues(conf, formUuid, fieldId, body_ext)`
-- **THEN** 可获取指定字段的可选项列表
-
-### Requirement: 批量操作
-
-`YDClient_Form` SHALL 提供批量新增与批量 upsert 两个方法,MUST 遵守宜搭侧每批 ≤ 100 条的限制。
-
-#### Scenario: 批量新增
-
-- **WHEN** `batchSaveForm(conf, formUuid, formDataListJson, body_ext)`
-- **THEN** 一次调用批量创建多条记录
-- **AND** 超过 100 条时必须由调用方自行分片(或方法内自动分片,策略见 Service 层)
-
-#### Scenario: 批量 upsert
-
-- **WHEN** `batchUpsertForm(conf, formUuid, searchConditionListJson, dataListJson, body_ext)`
-- **THEN** 对应批量 upsert
-- **AND** `searchCondition` 里日期字段 MUST 用字符串数组格式(规避 `selectListException`)
-
-### Requirement: 操作日志查询
-
-`YDClient_Form` SHALL 提供表单实例的操作历史查询方法。
-
-#### Scenario: 查询实例变更历史
-
-- **WHEN** `listFormOperations(conf, formInstanceId, body_ext)`
-- **THEN** 返回该实例的全部变更历史记录(含操作人 / 操作时间 / 操作类型)

+ 0 - 75
openspec/changes/archive/2026-04-19-extend-yida-api-coverage/specs/yida-process-atomic/spec.md

@@ -1,75 +0,0 @@
-## ADDED Requirements
-
-### Requirement: 流程实例生命周期
-
-`YDClient_Process` SHALL 覆盖流程发起、终止、撤回、跳转四个关键动作。每个方法 MUST 对应宜搭官方的一个流程实例 endpoint。
-
-#### Scenario: 发起流程
-
-- **WHEN** `startProcess(conf, processCode, formUuid, formDataJson, body_ext)`
-- **THEN** 对应 `POST /v1.0/yida/processes/instances/start`
-- **AND** 返回 `processInstanceId`
-
-#### Scenario: 终止流程
-
-- **WHEN** `terminateProcess(conf, processInstanceId, body_ext)`
-- **AND** `body_ext` 中支持 `operator` / `noExecuteExpression` 等
-
-#### Scenario: 撤回流程
-
-- **WHEN** `revokeProcess(conf, processInstanceId, body_ext)`
-- **THEN** 发起人撤回已提交但未审批完成的流程
-
-### Requirement: 审批任务动作
-
-`YDClient_Process` SHALL 覆盖同意 / 拒绝 / 转交 / 抄送 / 评论五类审批任务动作。每个方法 MUST 与宜搭官方 task 类 endpoint 1:1 对应。
-
-#### Scenario: 同意任务
-
-- **WHEN** `agreeTask(conf, processInstanceId, taskId, comment, body_ext)`
-- **THEN** 对应 `POST /v1.0/yida/processes/tasks/agree`
-
-#### Scenario: 拒绝任务
-
-- **WHEN** `disagreeTask(conf, processInstanceId, taskId, comment, body_ext)`
-- **AND** `body_ext` 支持 `nextOperatorUserIds`(拒绝后指定下一个审批人,部分流程开启此功能)
-
-#### Scenario: 转交任务
-
-- **WHEN** `redirectTask(conf, processInstanceId, taskId, toUserId, comment, body_ext)`
-
-#### Scenario: 抄送
-
-- **WHEN** `ccTask(conf, processInstanceId, taskId, toUserIds, comment, body_ext)`
-
-#### Scenario: 添加评论
-
-- **WHEN** `commentTask(conf, processInstanceId, taskId, comment, body_ext)`
-
-### Requirement: 流程与任务查询
-
-`YDClient_Process` SHALL 提供流程详情、流程列表、任务列表三类查询能力。
-
-#### Scenario: 查单个流程详情
-
-- **WHEN** `getProcess(conf, processInstanceId, body_ext)`
-- **THEN** 返回流程全量信息含已走过的所有节点
-
-#### Scenario: 分页查流程
-
-- **WHEN** `searchProcesses(conf, appType, formUuid, processCode, searchCriteria, body_ext)`
-- **THEN** 支持按发起人 / 状态 / 时间范围过滤
-
-#### Scenario: 查任务列表
-
-- **WHEN** `searchTasks(conf, userId, statuses, body_ext)`
-- **THEN** 返回某用户当前的待办 / 已办任务
-
-### Requirement: 节点跳转
-
-`YDClient_Process` SHALL 提供流程节点跳转能力,允许运维干预流程路径。
-
-#### Scenario: 跳转到指定节点
-
-- **WHEN** `redirectProcess(conf, processInstanceId, targetActivityId, body_ext)`
-- **THEN** 流程跳转到指定节点,支持前跳与后跳

+ 0 - 86
openspec/changes/archive/2026-04-19-extend-yida-api-coverage/tasks.md

@@ -1,86 +0,0 @@
-## 0. 覆盖度矩阵(按官方文档对齐前置工作)
-
-> 本节在实施前必须由开发者对照**当前**宜搭开放平台文档逐条填写。已知大致清单如下,具体参数以官方文档为准。
-
-### YDClient_Form 覆盖度
-
-| 官方 endpoint | HTTP | 方法名(建议) | 已对齐 | apiNote 链接 |
-|--------------|------|---------------|-------|-------------|
-| 新增表单实例 | POST | `saveForm` | ❌ | `/document/orgapp/add-or-update-form-instances` |
-| 更新表单实例 | PUT | `updateForm` | ❌ | 待补 |
-| 修改指定字段值 | PUT | `updateFormComponents` | ❌ | 待补 |
-| 删除表单实例 | DELETE | `deleteForm` | ❌ | 待补 |
-| 按条件删除 | POST | `deleteFormByCondition` | ❌ | 待补 |
-| 查单条 | GET | `getForm` | ❌ | 待补 |
-| 分页查询 | POST | `searchForm` | ❌ | `/document/orgapp/retrieve-search-form-information` |
-| 全量查询含子表 | POST | `listFormsAll` | ❌ | `/document/orgapp/retrieve-list-all` |
-| 查询 ID 列表 | POST | `listFormIds` | ❌ | 待补 |
-| 组件值查询 | POST | `listComponentValues` | ❌ | 待补 |
-| 批量新增 | POST | `batchSaveForm` | ❌ | 待补 |
-| 批量 upsert | POST | `batchUpsertForm` | ❌ | 待补 |
-| 查操作日志 | POST | `listFormOperations` | ❌ | 待补 |
-| 附件临时 URL | POST | `convertTempUrl` | ✅(旧 `convertTemporaryUrl`)| 迁移 |
-
-### YDClient_Process 覆盖度
-
-| 官方 endpoint | HTTP | 方法名 | 已对齐 | apiNote |
-|--------------|------|-------|-------|---------|
-| 发起流程 | POST | `startProcess` | ❌ | `/document/orgapp/initiate-a-process` |
-| 同意任务 | POST | `agreeTask` | ❌ | 待补 |
-| 拒绝任务 | POST | `disagreeTask` | ❌ | 待补 |
-| 转交任务 | POST | `redirectTask` | ❌ | 待补 |
-| 抄送 | POST | `ccTask` | ❌ | 待补 |
-| 评论任务 | POST | `commentTask` | ❌ | 待补 |
-| 终止流程 | POST | `terminateProcess` | ❌ | 待补 |
-| 撤回流程 | POST | `revokeProcess` | ❌ | 待补 |
-| 跳转节点 | POST | `redirectProcess` | ❌ | 待补 |
-| 查流程详情 | GET | `getProcess` | ❌ | 待补 |
-| 分页查流程 | POST | `searchProcesses` | ❌ | 待补 |
-| 查任务列表 | POST | `searchTasks` | ❌ | 待补 |
-
-## 1. 前置准备
-
-- [ ] 1.1 ⚠️ **阻塞**:官方文档 WebFetch 未成功(索引页),覆盖度矩阵基于 2026-04 训练知识 + 现有 `YDClientImpl` 已验证 URL。实施冒烟前必须对照官方文档逐条复核,尤其是 `processes/tasks/*` 与 `listApprovalRecords` 路径
-- [x] 1.2 新建 `com.malk.server.aliwork.YDAuth` 数据载体(已有 YDConf 是 @Component 单例不能直接当参数,故新建 YDAuth 作为原子接口的请求上下文)
-- [x] 1.3 现有 `YDParam` 建造者保留不动;新原子接口直接用 `Map<String,Object> body_ext` 模式,语义更清晰
-
-## 2. YDClient_Form 接口
-
-- [x] 2.1 新建 `mjava/src/main/java/com/malk/service/aliwork/YDClient_Form.java` 接口
-- [x] 2.2 声明 16 个方法(14 必须 + `remarkForm` 评论 + `convertTempUrl` 附件临时 URL 从旧 YDClient 迁入)
-- [x] 2.3 每个方法 javadoc 写全 `@apiNote` + `body_ext` 字段枚举
-
-## 3. YDClient_Form 实现
-
-- [x] 3.1 新建 `impl/YDClient_FormImpl.java`
-- [x] 3.2 每个方法通过 `UtilHttp.doPost/doGet/doPut/doDelete` 实现;update 走 PUT,delete 走 DELETE
-- [x] 3.3 返回值 `String` / `Map<String,Object>` / `List<Map<String,Object>>` / `Boolean` 按官方语义选择
-- [x] 3.4 错误处理:`assertResult` 统一抛 `McException` 带宜搭原始错误码
-
-## 4. YDClient_Process 接口与实现
-
-- [x] 4.1 新建 `YDClient_Process.java` 接口
-- [x] 4.2 声明 11 个方法(startProcess / terminateProcess / revokeProcess / redirectProcess / agreeTask / disagreeTask / redirectTask / ccTask / commentTask / getProcess / searchProcesses / listProcessIds / listApprovalRecords — 实际 13 个,超额覆盖 design.md 最小要求)
-- [x] 4.3 新建 `impl/YDClient_ProcessImpl.java`
-
-## 5. YDService 切换
-
-- [ ] 5.1 `YDService` 公开签名不变,内部将 `operateData` / `queryData` 分支调用改为调原子方法
-- [ ] 5.2 保留旧 `YDClient.operateData` / `queryData` 原样
-- [ ] 5.3 回归测试:mcli / shunfeng / guangming 三个客户不回归
-
-## 6. 文档与审计
-
-- [ ] 6.1 审计日志(`UtilHttp` §3.5)在新原子方法生效时验证字段齐全
-- [ ] 6.2 更新 `/Users/malk/Desktop/Tech/claude/后端/yida-serverside.md`,把"YDClient 用法"替换为新原子方法示例
-
-## 7. 验证
-
-- [ ] 7.1 单元测试覆盖所有 26 个方法的参数透传(mock `UtilHttp` 校验 body 含所有传入字段)
-- [ ] 7.2 集成测试:至少对 3 个关键 endpoint(saveForm / startProcess / agreeTask)跑真实宜搭调用
-- [ ] 7.3 `openspec validate extend-yida-api-coverage --strict` 通过
-
-## 8. 交付
-
-- [ ] 8.1 PR 附覆盖度矩阵对照截图
-- [ ] 8.2 走 `/opsx:archive` 归档

+ 0 - 43
openspec/changes/archive/2026-04-19-init-project-baseline/design.md

@@ -1,43 +0,0 @@
-## 设计原则
-
-本次基线沉淀的取舍原则:
-
-1. **文档中心 vs 仓库内 spec**:规范主体放在 `/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md`,因为那里能被跨仓库(包括 akds / mjava-guangming / 未来新客户项目)共享引用;仓库内 `openspec/specs/project-baseline.md` 只做指针 + 代码锚点,避免两地维护同一份规范。
-2. **保障服务可用优先**:现有三家客户(mcli/shunfeng/guangming)均在生产运行,本次任何动作**不得影响已部署的 jar 行为**。因此禁止任何代码改动、pom 改动、配置改动。
-3. **YAGNI**:第一阶段明确不引入 Redis / Docker / 消息队列 / K8s / 链路追踪中间件。凡有"万一将来要用"的念头,都押后到有真实触发条件时再评估。
-
-## 技术约束(已在基座固化,本次只是文档化)
-
-| 维度 | 约定 | 实现锚点 |
-|------|------|----------|
-| HTTP 请求 | 统一 `UtilHttp`(Hutool 封装),禁三方 SDK | `mjava/src/main/java/com/malk/utils/UtilHttp.java` |
-| Token 缓存 | 统一 `UtilToken` (TimedCache),禁每请求重取 | `mjava/src/main/java/com/malk/utils/UtilToken.java` |
-| 鉴权参数来源 | 只读 `application-*.yml`,禁硬编码/请求入参传入 | `@ConfigurationProperties` |
-| 响应结构 | Controller 返 `McR<T>`,第三方映射 `VenR` | `com.malk.server.common/` |
-| 全局异常 | `CatchException` 拦截;业务抛 `McException` | `com.malk.filter.CatchException` |
-| 持久化 | JPA + QueryDSL + `BaseRepository` + `BasePo`;禁 MyBatis 混用 | `com.malk.base/` |
-| 链路追踪 | `TraceIdFilter` + logback `%X{traceId:-}` | `com.malk.filter.TraceIdFilter` |
-| 配置管理 | `application-{dev,test,prod}.yml`;敏感走环境变量 | `mjava/src/main/resources/` |
-
-## 子项目接入模型
-
-```
-java-mcli (根 pom,packaging=pom)
- ├── mjava (基座 jar,无 repackage,可被依赖)
- ├── mjava-mcli (客户模板:最小化 Boot.java + application-*.yml)
- ├── mjava-shunfeng (客户:会议聚合)
- └── mjava-guangming (客户:钉钉 SSO 邮箱)
-```
-
-新客户接入 = 复制 `mjava-mcli` → 改 artifactId/port/context-path → 根 pom `<modules>` 追加。
-
-启动类必须 `@SpringBootApplication(scanBasePackages = {"com.malk"})`,才能扫到 mjava 基座所有 bean。
-
-## Non-Goals(本次设计不覆盖)
-
-- Redis 引入后的缓存重构方案
-- Docker 镜像与 CI/CD 流水线
-- 多租户与权限系统抽象
-- 基于 `mjava-baseline` 做一份 IDE 模板或 archetype
-
-这些话题等有真实需求时再各自立新 change。

+ 0 - 43
openspec/changes/archive/2026-04-19-init-project-baseline/proposal.md

@@ -1,43 +0,0 @@
-> 状态(2026-04-18 新建):**文档提案,无代码动作**
-
-## Why
-
-`mjava-ai` 仓库已稳定承载三个生产客户(mcli / shunfeng / guangming),基座 `mjava` 的核心能力(`UtilHttp` / `UtilToken` / `McR` / JPA+QueryDSL / 全局异常 / TraceId)已经形成事实规范但没有被文档化。新成员或新客户接入时,缺少一份"权威基线"文档可以指向。
-
-同时,`openspec/specs/` 目录此前为空,OpenSpec 工作流缺少稳态规范锚点。现需要沉淀一份指针式的 baseline spec,把开发规范主权交给文档中心 `/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md`,仓库内只保留"差异 + 代码锚点"。
-
-## What Changes
-
-**本提案不改任何 Java 源码、不改 pom、不 push git**,仅沉淀规范与索引:
-
-- 新建 `openspec/specs/project-baseline.md` —— 一页纸稳态 spec,指向文档中心的权威规范,并列出仓库内关键代码锚点
-- 新建仓库根 `CLAUDE.md` —— 给未来在该仓库工作的 AI / 新成员一个 40 行以内的入口,链到 openspec 工作流与文档中心
-- 通过 `/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md` 权威化 11 项规范(HTTP / Token / 配置 / 响应异常 / 持久化 / 日志链路 / 子项目接入 / 构建 / YAGNI 清单 等)
-
-## Capabilities
-
-### New Capabilities
-- `project-baseline`: 项目基线规范锚点(仓库内 specs/ 层),定义"什么是 mjava-ai 的标准开发方式"
-
-### Modified Capabilities
-<!-- 无 -->
-
-## Impact
-
-- **代码影响**:零
-- **pom 影响**:零
-- **配置影响**:零
-- **对生产**:零(mcli/shunfeng/guangming 运行不受任何影响)
-- **对新客户接入**:有正向指引——按 `mjava-baseline.md` 第 9 章复制 `mjava-mcli` 模板即可
-- **对 OpenSpec 工作流**:`openspec/specs/` 不再为空,后续新 change 可以 reference baseline
-
-## Non-Goals
-
-显式声明**本次不做**:
-
-- ❌ 删除或改造 `mjava-shunfeng` / `mjava-guangming` 子模块(保障生产可用)
-- ❌ 修改根 pom 的 `<modules>` 聚合
-- ❌ 升级 `mjava.version`
-- ❌ 引入 Redis / Docker / 消息队列 / K8s(见 `mjava-baseline.md` 第 11 章)
-- ❌ 为新客户搭空脚手架(等实际需求来临再走 `/opsx:propose`)
-- ❌ git commit / push / 设置 remote

+ 0 - 41
openspec/changes/archive/2026-04-19-init-project-baseline/spec.md

@@ -1,41 +0,0 @@
-## 基线规范(本仓库视角)
-
-本仓库 `mjava-ai` 采纳 mjava 基座通用后端开发规范。权威版本维护在文档中心:
-
-**权威文档**:`/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md`(11 章)
-
-本 spec 只记录**仓库内差异点**与**代码锚点**,不复述规范正文。
-
-### 仓库内现状差异
-
-| 项目 | 规范要求 | 本仓库现状 | 差距 |
-|------|---------|-----------|------|
-| 健康检查端点 | `/actuator/health` 可访问 | 已新增依赖与配置,未生产冒烟 | 见 `changes/add-observability-foundation` |
-| TraceId 链路 | 每条日志带 traceId | 已实施 filter + logback pattern | 见 `changes/add-observability-foundation` |
-| RSA 加密工具 | 基座 `com.malk.util.crypto` | 已下沉,旧位置留委托壳 | 见 `changes/extract-dingtalk-standard-api` |
-
-### 代码锚点
-
-| 规范章节 | 代码位置 |
-|---------|---------|
-| HTTP 调用 (mjava-baseline §3) | `mjava/src/main/java/com/malk/utils/UtilHttp.java` |
-| Token 缓存 (§4) | `mjava/src/main/java/com/malk/utils/UtilToken.java` |
-| 响应与异常 (§6) | `mjava/src/main/java/com/malk/server/common/{McR,VenR,McException,McPage}.java` |
-| 全局异常拦截 (§6) | `mjava/src/main/java/com/malk/filter/CatchException.java` |
-| 持久化基类 (§7) | `mjava/src/main/java/com/malk/base/{BaseRepository,BasePo,BaseDto}.java` |
-| 链路追踪 (§8) | `mjava/src/main/java/com/malk/filter/TraceIdFilter.java` |
-| 多数据源 (§5) | `mjava/src/main/java/com/malk/config/DataSourceConfig.java` |
-| 客户子项目模板 (§9) | `mjava-mcli/` |
-
-### 子项目清单
-
-| 子模块 | 状态 | 端口/context-path |
-|--------|------|-------------------|
-| `mjava` | 基座 jar | - |
-| `mjava-mcli` | 模板 + 最小实例 | 9001 / /api/mcli |
-| `mjava-shunfeng` | 已交付生产 | 见 `mjava-shunfeng/application-prod.yml` |
-| `mjava-guangming` | 已交付生产 | 见 `mjava-guangming/application-prod.yml` |
-
-### 变更边界
-
-规范正文修改(如新增第 12 章、调整 HTTP 封装约定)必须走 `/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md` 的编辑 + 本仓库 `/opsx:propose` 新建对应 change。**不在本 spec 内直接编辑规范正文。**

+ 0 - 28
openspec/changes/archive/2026-04-19-init-project-baseline/tasks.md

@@ -1,28 +0,0 @@
-## 1. 文档中心落地
-
-- [x] 1.1 新建 `/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md`(11 章通用基座规范)
-- [x] 1.2 原 `后端/CLAUDE.md` 拆为 `后端/yida-serverside.md` + 新 `后端/CLAUDE.md`(总索引)
-- [x] 1.3 上层 `/Users/malk/Desktop/Tech/claude/CLAUDE.md` 同步更新索引与 server 分层说明
-
-## 2. 仓库内 spec 锚点
-
-- [x] 2.1 新建 `openspec/specs/project-baseline.md`(单文件,指向文档中心)
-- [x] 2.2 本目录 `proposal.md` / `design.md` / `spec.md` 四件套齐备
-- [x] 2.3 现有 `changes/add-observability-foundation/proposal.md` 顶部加状态标注
-- [x] 2.4 现有 `changes/extract-dingtalk-standard-api/proposal.md` 顶部加状态标注
-
-## 3. 仓库根入口
-
-- [x] 3.1 新建 `/Users/malk/server/cur/mjava-ai/CLAUDE.md`(40 行以内,链 openspec + 文档中心)
-
-## 4. auto-memory 路径同步
-
-- [x] 4.1 检查并更新 `~/.claude/projects/-Users-malk/memory/` 下涉及 `/Users/malk/server/{mjava-ai,akds,mjava-guagnming}` 绝对路径的条目
-
-## 5. 验证
-
-- [x] 5.1 `ls /Users/malk/server/cur/mjava-ai/openspec/specs/` 含 `project-baseline.md`
-- [x] 5.2 `ls /Users/malk/Desktop/Tech/claude/后端/` 含 `CLAUDE.md` `mjava-baseline.md` `yida-serverside.md`
-- [x] 5.3 `ls /Users/malk/server/` 只剩 `_Tool cur pre`
-- [x] 5.4 `mvn -pl mjava-mcli,mjava-shunfeng,mjava-guangming -am compile` 全部通过(2026-04-19 验证)
-      — 顺带修复两处 pre-existing 编译歧义:ZoomClientImpl 的 `UtilHttp.doGet(url, header, null)` 与 MeetingServiceImpl 的 `UtilMap.map(summary,start,end, subject...)` 重载冲突,显式 cast 后编译通过

+ 0 - 86
openspec/changes/archive/2026-04-26-add-integration-user-api/design.md

@@ -1,86 +0,0 @@
-# 集成平台用户接口设计
-
-> 文档来源:apifox 公开站 `https://uj0cyahyvs.apifox.cn/`(共 29 个 API;本期取 5 个)
-> 抓取时间:2026-04-26
-
-## 覆盖度矩阵
-
-| 接口 | apifox ID | Method | Path | 本期 |
-|------|-----------|--------|------|------|
-| 获取 access_token(请求体方式) | 288915790 | POST | `/iam/token` | ✅ |
-| 获取 access_token(Basic 方式) | 288915791 | POST | `/iam/token/basic` | ⏸ 暂不实现,请求体方式更通用 |
-| 创建用户 | 288915800 | POST | `/iam/api/users` | ✅ |
-| 修改用户 | 288915803 | PATCH | `/iam/api/user` | ✅ |
-| 删除用户 | 288915805 | POST | `/iam/api/users/delete` | ✅ |
-| 查询用户 | 288915799 | GET | `/iam/api/users` | ✅(顺手对齐,便于联调验证) |
-| 其它 23 个接口(org / 角色 / Webhook / 连接器 / token basic / 启用禁用 / 改密码 / ...) | 288915790-288915818 | - | - | ⏸ 后续按需另立 change |
-
-## 关键决策
-
-### D1. 鉴权方式选「请求体」而非「Basic」
-
-两种方式服务端等价。选请求体方式(`/iam/token`)原因:
-- `Basic base64(client_id:client_secret)` 需要在客户端做编码 + Header 注入,封装复杂度更高
-- 请求体方式直接 form body 三个字段,与 mjava 现有 vendor token 拉取风格一致
-
-### D2. token 缓存走 `UtilToken`,namespace=`integration`
-
-- key:`integration:accessToken`(基座单租户场景,无需拼 clientId)
-- TTL:服务端返回 `expires_in`,本地缓存设为 `expires_in - 60`(提前 60s 失效,避免边界问题)
-- 缓存 miss 或过期 → `IntpClientImpl.getAccessToken()` 同步拉新
-
-### D3. Client 方法签名遵循 baseline §3.4.2
-
-- 第一个参数始终是 `String accessToken`(按现有 dingtalk/aliwork Client 风格)
-- 必填字段显式入参(`username`、`password`、`usernames`)
-- 可选字段统一打入 `Map<String, Object> body_ext`,javadoc 完整枚举字段名 + 类型 + 含义
-
-具体签名(`INTPClient_User`):
-
-```java
-String getAccessToken();                                            // 自带缓存,调用方第一步先取
-Map createUser(String access_token, String username, String password,
-               Map body_ext);    // body_ext: name/email/phone_number/user_job_number/nick_name/picture/org_ids/address/title/hired_date/tag/group_positions
-Boolean updateUser(String access_token, String username,
-               Map body_ext);    // username 走 Query Param,body 全是可选字段
-Boolean deleteUsers(String access_token, List<String> usernames);   // body: { "usernames": [...] }
-Map queryUsers(String access_token, Map query);                     // query: q/org_id/page/size/attrs/return_users_in_sub_org
-```
-
-返回类型沿用 mjava 现有 Client 风格(如 `DDClient_Group.createGroup` 返回 `Map`、`addGroupUser` 返回 `Boolean`)。`getAccessToken()` 进接口,调用方先取再调 4 个 CRUD 方法,与 dingtalk `DDService.getAccessToken()` 模式对齐。
-
-### D4. 响应 unwrap 策略
-
-集成平台统一响应:`{ result, error, error_description, data? }`。沿用 `DDR` / `YDR` 模式新建 `INTPR extends VenR`:
-
-- `INTPR.assertSuccess()` 在 `result=false` 时抛 `McException(error, error_description, "integration")`
-- 由全局 `CatchException` 统一拦截转 HTTP 4xx/5xx
-- Client 方法直接返回 `(Map) intpR.getData()` 或 `intpR.isResult()`(参考 `DDR.doPost(...).isSuccess()` 用法)
-
-### D5. 配置段命名
-
-`application.yml` 增加 `integration:` 段,与 `dingtalk:` `aliwork:` 平级:
-
-```yaml
-integration:
-  baseUrl: https://iam.example.com    # 集成平台域名(不含路径前缀)
-  clientId: ${INTP_CLIENT_ID:}
-  clientSecret: ${INTP_CLIENT_SECRET:}
-```
-
-`baseUrl` 不含 `/iam/` 前缀,由 Client 内部拼接,便于将来扩到 `/acm/` 域。
-
-### D6. 不做的事(明确 yagni 边界)
-
-- ❌ 高阶 Service 封装(用户明确"不需要",调用方自己组合)
-- ❌ 多租户(每租户一套凭据) —— 留待 mjava-pro `DynamicIntpService` 后续专项
-- ❌ Webhook 接收 `/iam/api/open/event/...` —— 不在用户域内
-- ❌ 单元测试 / 集成冒烟 —— 阻塞在 Maven,后续阶段做
-
-## 风险
-
-1. **apifox 文档与生产 API 漂移**:apifox 是文档站,与实际生产环境是否一致需联调验证。建议接入第一个客户时立即抓真实 access_token 跑一次 createUser。
-2. **`/iam/api/user` 单数 vs `/iam/api/users` 复数**:修改用户 endpoint 是 `/iam/api/user`(单数),与其它 CRUD 不一致。已按文档原样实现,**实施冒烟时需复核**。
-3. **`updateUser` 的 `username` 是 Query 而非 Path**:与 RESTful 惯例不同,不要写成 `/iam/api/user/{username}`。
-4. **`deleteUsers` 用 POST 而非 DELETE**:因为 body 要带数组;按文档原样保留。
-5. **token 失效处理**:本期仅做"过期前刷新"。如果服务端单边失效(管理员 revoke),调用会拿到 401,本期不做"401 自动重拿"重试链,由调用方处理。

+ 0 - 37
openspec/changes/archive/2026-04-26-add-integration-user-api/proposal.md

@@ -1,37 +0,0 @@
-> 状态(2026-04-26 立项):**规则定义阶段**
-> 优先级:中(Phase B.1 第三个 vendor 专项)
-> 依赖:基座 `UtilHttp` / `UtilToken` / `McR` 已就绪
-
-## Why
-
-后续多个客户项目要调用「集成平台」(IAM / 身份治理产品)的用户管理接口,做"钉钉用户 → 集成平台用户"的同步与对接。集成平台官方接口为 RESTful + OAuth2 client_credentials 模式,与 mjava 现有 vendor(dingtalk/aliwork/...)模式不同,需要:
-
-- 独立的 `service/integration/` 包承载;类前缀 `INTPClient_*` / `INTPConf` / `INTPR`(与 dingtalk DD* / aliwork YD* 全大写惯例一致)
-- 独立的 `IntpConf`(baseUrl / clientId / clientSecret),配置段 `integration.*` 与 dingtalk/aliwork 平级
-- token 走 `UtilToken` 缓存,按"过期前刷"策略(OAuth2 返回 `expires_in`)
-
-本 change **只覆盖用户域写操作 + 鉴权**:getAccessToken / createUser / updateUser / deleteUser。其它能力(org、角色、查询、Webhook、连接器事件)按需另立 change。
-
-## What Changes
-
-新增 vendor 包:
-
-- `server/integration/INTPConf.java`:配置类(baseUrl / clientId / clientSecret)
-- `server/integration/INTPR.java`:响应包装(继承 `VenR`),字段 `result/error/error_description/data` + `assertSuccess`
-- `service/integration/INTPClient_User.java`(接口):4 个对外原子方法
-  - `createUser(access_token, username, password, body_ext)`
-  - `updateUser(access_token, username, body_ext)`
-  - `deleteUsers(access_token, usernames)`
-  - `queryUsers(access_token, query)` —— 顺手对齐 GET 接口,便于联调校验
-- `service/integration/impl/INTPImplClient_User.java`:基于 `INTPR.doPost/doGet/doPatch` + `UtilToken` 缓存 access_token 的实现(`getAccessToken()` 为 private 方法,不进 Client 接口)
-- 三个客户子模块的 `application-{dev,prod}.yml.example` 加 `integration:` 段占位
-
-不涉及:
-- mjava-pro 多租户化(按用户决策"基座单租户")
-- Service 层业务封装(按用户决策"不需要")
-- 其它 18 个非用户接口(org / 角色 / Webhook / 连接器)
-
-## Capabilities
-
-### New Capabilities
-- `integration-user-api`:集成平台用户域的鉴权与 CRUD 原子接口集

+ 0 - 85
openspec/changes/archive/2026-04-26-add-integration-user-api/specs/integration-user-api/spec.md

@@ -1,85 +0,0 @@
-## ADDED Requirements
-
-### Requirement: 集成平台 OAuth2 鉴权与 token 缓存
-
-`INTPImplClient_User#getAccessToken()` SHALL 通过 `client_credentials` 流程获取 access_token,并使用 `UtilToken` 缓存。CRUD 方法 MUST 接收 `access_token` 作为第一个参数,调用方先调 `getAccessToken()` 再传入。
-
-#### Scenario: 首次拉取 access_token
-
-- **WHEN** `UtilToken` 缓存中无 `integration:accessToken`
-- **THEN** 向 `{baseUrl}/iam/token` 发 POST,body 为 `application/x-www-form-urlencoded`,字段:`grant_type=client_credentials` / `client_id={IntpConf.clientId}` / `client_secret={IntpConf.clientSecret}`
-- **AND** 响应 `access_token` 写入缓存,TTL = `expires_in - 60` 秒
-
-#### Scenario: 缓存命中
-
-- **WHEN** `UtilToken` 缓存中已有未过期的 `integration:accessToken`
-- **THEN** 直接复用,不发 HTTP 请求
-
-#### Scenario: token 失效
-
-- **WHEN** 服务端 revoke 或过期
-- **THEN** 调用方收到 HTTP 401 + `error/error_description`
-- **AND** 本期不做"401 自动重拿",由调用方决定重试策略
-
-### Requirement: 用户创建
-
-`INTPClient_User.createUser` SHALL 对齐 apifox 接口 `POST /iam/api/users`。必填字段必须显式入参;可选字段通过 `body_ext` Map 注入。
-
-#### Scenario: 创建用户(最小集)
-
-- **WHEN** 调用 `createUser(accessToken, "alice", "Pwd@123", null)`
-- **THEN** 向 `{baseUrl}/iam/api/users` 发 POST `application/json`
-- **AND** Header `Authorization: Bearer {accessToken}`
-- **AND** body 为 `{"username":"alice","password":"Pwd@123"}`
-
-#### Scenario: 创建用户(完整字段)
-
-- **WHEN** `body_ext` 含 `name` / `email` / `phone_number` / `user_job_number` / `nick_name` / `picture` / `org_ids` / `address` / `title` / `hired_date` / `tag` / `group_positions`
-- **THEN** body 合并 `username` / `password` 与 `body_ext` 全部字段
-- **AND** javadoc MUST 枚举上述字段名 + 类型 + 含义
-
-#### Scenario: 创建失败
-
-- **WHEN** 服务端返回 `{"result":false,"error":"USER_EXIST","error_description":"用户已存在"}`
-- **THEN** 返回 `McR.error("USER_EXIST", "用户已存在")`
-
-### Requirement: 用户修改
-
-`INTPClient_User.updateUser` SHALL 对齐 apifox 接口 `PATCH /iam/api/user`。`username` 通过 Query Param 传递(**非 Path**),其它字段全部走 body。
-
-#### Scenario: 修改单字段
-
-- **WHEN** 调用 `updateUser(accessToken, "alice", Map.of("email", "alice@new.com"))`
-- **THEN** 向 `{baseUrl}/iam/api/user?username=alice` 发 PATCH
-- **AND** body 为 `{"email":"alice@new.com"}`
-- **AND** 不传的字段保持服务端原值不变
-
-### Requirement: 用户删除(批量)
-
-`INTPClient_User.deleteUsers` SHALL 对齐 apifox 接口 `POST /iam/api/users/delete`。**HTTP 方法是 POST**,因 body 需带 username 数组。
-
-#### Scenario: 批量按登录名删除
-
-- **WHEN** 调用 `deleteUsers(accessToken, List.of("alice","bob"))`
-- **THEN** 向 `{baseUrl}/iam/api/users/delete` 发 POST `application/json`
-- **AND** body 为 `{"usernames":["alice","bob"]}`
-
-### Requirement: 用户查询
-
-`INTPClient_User.queryUsers` SHALL 对齐 apifox 接口 `GET /iam/api/users`,支持分页与多维过滤。
-
-#### Scenario: 关键字搜索
-
-- **WHEN** 调用 `queryUsers(accessToken, Map.of("q","alice","page",1,"size",20))`
-- **THEN** 向 `{baseUrl}/iam/api/users?q=alice&page=1&size=20` 发 GET
-- **AND** 解析响应 `data.items[]` 返回 `McR.success(data)`
-
-### Requirement: 配置段独立命名
-
-`INTPConf` SHALL 通过 Spring `@ConfigurationProperties("integration")` 加载配置;`baseUrl` 不含 `/iam` 前缀。
-
-#### Scenario: 三个客户子模块的 yml 模板
-
-- **WHEN** 浏览 `mjava-mcli` / `mjava-shunfeng` / `mjava-guangming` 的 `application-{dev,prod}.yml.example`
-- **THEN** 每个模板 MUST 含 `integration:` 段示例,字段:`baseUrl` / `clientId` / `clientSecret`
-- **AND** 真实凭据从 `${INTP_CLIENT_ID}` / `${INTP_CLIENT_SECRET}` 环境变量注入

+ 0 - 46
openspec/changes/archive/2026-04-26-add-integration-user-api/tasks.md

@@ -1,46 +0,0 @@
-## 0. 文档抓取(前置)
-
-> 已完成:apifox 公开站抓 5 个接口字段详情,结果固化在 `design.md` 覆盖度矩阵 + 风险章节。
-> 风险:apifox 文档可能与生产 API 漂移,实施冒烟时需联调验证。
-
-## 1. 配置类 + 响应包装 + yml 段
-
-- [x] 1.1 新建 `mjava/src/main/java/com/malk/server/integration/INTPConf.java`,`@ConfigurationProperties("integration")`,字段 `baseUrl` / `clientId` / `clientSecret` + 4 个 path 常量 + 缓存 key — 2026-04-26
-- [x] 1.2 新建 `mjava/src/main/java/com/malk/server/integration/INTPR.java`,extends `VenR`,字段 `result` / `error` / `error_description` / `data` + `access_token`/`expires_in`(兼容 token endpoint)+ `assertSuccess` 抛 `McException` + 静态 `doPost/doGet` — 2026-04-26
-- [x] 1.3 `VenR` 加 `RC_INTP = "com.malk.server.integration.INTPR"` 常量 — 2026-04-26
-- [x] 1.4 `mjava/src/main/resources/application-prod.yml.example` 加 `integration:` 段 — 2026-04-26
-- [x] 1.5 `mjava-mcli` / `mjava-shunfeng` / `mjava-guangming` 三个 `application-prod.yml.example` 加 `integration:` 段 — 2026-04-26
-- [x] 1.6 真实 dev/prod yml 不动(敏感凭据由用户自填)
-
-## 2. Client 接口与实现
-
-- [x] 2.1 新建 `service/integration/INTPClient_User.java`(接口),5 方法(含 `getAccessToken()`) — 2026-04-26
-- [x] 2.2 javadoc 完整枚举 `body_ext` 与 `query` 的可选字段(含子对象字段如 tag/group_positions) — 2026-04-26
-- [x] 2.3 新建 `service/integration/impl/INTPImplClient_User.java`,依赖 `INTPConf` — 2026-04-26
-- [x] 2.4 `getAccessToken()` 公开方法:缓存命中返回;miss 调 `POST {baseUrl}/iam/token` 走 form body 拿 token,TTL=`(expires_in - 60) * 1000` 毫秒入 `UtilToken.put` — 2026-04-26
-- [x] 2.5 4 个 CRUD 方法实现:拼 URL + Header `Authorization: Bearer` + `INTPR.doPost/doGet` 或 `UtilHttp.doPatch(... INTPR.class)` + `INTPR.assertSuccess` 自动抛业务异常 — 2026-04-26
-
-## 3. 验证
-
-- [x] 3.1 `mvn -pl mjava -am clean compile` 通过(2.5s,仅历史 warning) — 2026-04-26
-- [x] 3.2 `mvn clean package -DskipTests` 全 reactor 7 模块 BUILD SUCCESS(5.5s) — 2026-04-26
-- [ ] 3.3 联调冒烟(任选一个客户环境,待真实凭据):
-      - 拿 clientId/clientSecret 配进 dev yml
-      - 写一个临时 Controller 调 `getAccessToken()` + `createUser(token, "test_user", "Pwd@123", null)`
-      - 验证服务端真实创建一条记录 + 返回数据正确
-      - 重复调用验证 token 缓存命中(不再发 `/iam/token` 请求)
-- [ ] 3.4 `openspec validate add-integration-user-api --strict` 通过(待 openspec CLI 可用时跑)
-
-## 4. 归档
-
-- [ ] 4.1 `/opsx:archive add-integration-user-api` → 移到 `openspec/changes/archive/2026-04-XX-add-integration-user-api/`
-- [ ] 4.2 spec 合并到 `openspec/specs/integration-user-api/`
-- [ ] 4.3 BACKLOG.md 标 ✅;mjava-baseline.md vendor 表加一行 integration
-
-## 5. 后续 change(不在本期范围)
-
-- 集成平台 org / 部门管理(`/iam/api/orgs/*`)
-- 集成平台 角色管理(`/iam/api/tags/*`)
-- 集成平台 启用/禁用用户 + 改密码(`/iam/api/users/status` / `/iam/api/users/password`)
-- Webhook 接收(`/iam/api/open/event/...`)
-- mjava-pro 多租户 `DynamicIntpService`

+ 0 - 84
openspec/changes/rename-dingtalk-impl-suffix/design.md

@@ -1,84 +0,0 @@
-# Design
-
-## 关键决策
-
-### 1. 为什么分批 4 批
-
-14 文件一把改的成本:
-
-- grep 引用清单 ≥ 50 行(钉钉是基座最重的接入),用户审阅压力大
-- 一次 PR 影响所有钉钉调用方,回滚必须全量
-- 任一引用方漏改 → 编译爆炸 → 整个仓库阻塞
-
-分批 4 批的好处:
-
-- 每批 3~4 文件,grep 清单 ≤ 15 行
-- 每批独立 commit,回滚成本 = 1 批
-- 用户可在批次间叫停("批次 1 出问题,先观察一周再继续")
-- 失败半径限定在该批关联调用方
-
-### 2. 批次顺序为什么这样排
-
-| 批次 | 文件 | 排序理由 |
-|---|---|---|
-| 1 | `DDImplClient` / `DDImplService` / `DDImplClient_Attendance` | 主流量入口,先打通主路径验证迁移可行性 |
-| 2 | `DDImplClient_Contacts` / `_Group` / `_Notice` | 通讯通知,高频但相对独立 |
-| 3 | `DDImplClient_Personnel` / `_Report` / `_Schedule` | 人事日历,与考勤强关联 |
-| 4 | `DDImplClient_Dedicated` / `_Event` / `_Extension` / `_Storage` / `_Workflow` | 长尾,引用方少,最后清扫 |
-
-### 3. Spring Bean 名 by-name 兼容性
-
-Spring 默认 Bean 名是类名首字母小写。改名后:
-
-| 旧 Bean 名 | 新 Bean 名 |
-|---|---|
-| `dDImplClient` | `dDClientImpl` |
-| `dDImplClient_Attendance` | `dDClient_AttendanceImpl` |
-| ... | ... |
-
-调用方影响:
-- `@Autowired DDClient ddClient`(by 接口类型)— **不受影响**,Spring 仍能找到唯一实现
-- `@Autowired @Qualifier("dDImplClient") DDClient client`(by-name)— **受影响**,必须改
-- `@Resource(name = "dDImplClient_Attendance")` — **受影响**,必须改
-
-每批 R4 grep 时必须加扫 `@Qualifier` `@Resource(name=` 字面量。
-
-### 4. 没用 `@Component("dDImplClient_Attendance")` 别名兼容
-
-可以给新类标 `@Component("dDImplClient_Attendance")` 保留旧 Bean 名,让调用方零修改。**拒绝**,原因:
-
-- 旧 Bean 名继续存在会让代码长期维持「类名后缀 + Bean 名中缀」的语义裂缝,违反 R5 精神
-- 别名是临时栈,迟早要拆 → 拆别名时还得做一遍引用扫,反复做 2 次
-- 一次性切换的总成本 ≤ 双轨过渡
-
-### 5. 跨仓 grep 范围
-
-- mjava-ai:必扫
-- 光明独立仓 `/Users/malk/server/cur/mjava-guangming/`:含 mjava-guangming/mjava-shunfeng 模块 — 必扫
-- akds 项目:路径在用户 memory 中(独立维护),ACK 时由用户确认是否扫
-- 其他客户独立仓:用户 ACK 时主动补充清单
-
-grep 命令模板(每批执行):
-
-```bash
-# 该批每个旧类名扫:
-grep -rE "(import\s+.+\.DDImplClient_Attendance|DDImplClient_Attendance\.class|@Qualifier\(\"dDImplClient_Attendance\"|@Resource\(name\s*=\s*\"dDImplClient_Attendance)" \
-  --include="*.java" \
-  /Users/malk/server/cur/mjava-ai/ \
-  /Users/malk/server/cur/mjava-guangming/ \
-  {其他客户仓}
-```
-
-### 6. 验证流程(每批)
-
-1. 改名 + import 替换
-2. `mvn -pl mjava -am clean compile`(基座)
-3. `mvn -pl mjava-mcli,mjava-pro,mjava-com -am clean compile`(子项目)
-4. 跨仓客户项目验证(用户在 ACK 时指定)
-5. 抽样运行:钉钉考勤/通讯录最常用接口的集成测试
-
-## 拒绝的方案
-
-- **一次 commit 14 文件** — 拒绝。爆炸半径过大
-- **同时改 `DDClient` 接口名** — 拒绝。R5 只约束 Impl 命名;接口名变化触发跨仓所有 import 变化,超出本 change 范围
-- **保留旧 Bean 名作为别名** — 拒绝(见 §4)

+ 0 - 73
openspec/changes/rename-dingtalk-impl-suffix/proposal.md

@@ -1,73 +0,0 @@
-> 状态(2026-06-10 立项):**占位提案,未排期**
-> 优先级:中。依赖 `standardize-client-service-layering` 先 archive 入 R5;本 change 是 R5 对存量钉钉 14 文件的整改。
-
-## Why
-
-R5(命名一致,`Impl` 后缀)落地后,钉钉模块仍有 14 个存量中缀文件违反规则:
-
-```
-service/dingtalk/impl/DDImplClient.java               → DDClientImpl.java
-service/dingtalk/impl/DDImplService.java              → DDServiceImpl.java
-service/dingtalk/impl/DDImplClient_Attendance.java    → DDClient_AttendanceImpl.java
-service/dingtalk/impl/DDImplClient_Contacts.java      → DDClient_ContactsImpl.java
-service/dingtalk/impl/DDImplClient_Dedicated.java     → DDClient_DedicatedImpl.java
-service/dingtalk/impl/DDImplClient_Event.java         → DDClient_EventImpl.java
-service/dingtalk/impl/DDImplClient_Extension.java     → DDClient_ExtensionImpl.java
-service/dingtalk/impl/DDImplClient_Group.java         → DDClient_GroupImpl.java
-service/dingtalk/impl/DDImplClient_Notice.java        → DDClient_NoticeImpl.java
-service/dingtalk/impl/DDImplClient_Personnel.java     → DDClient_PersonnelImpl.java
-service/dingtalk/impl/DDImplClient_Report.java        → DDClient_ReportImpl.java
-service/dingtalk/impl/DDImplClient_Schedule.java      → DDClient_ScheduleImpl.java
-service/dingtalk/impl/DDImplClient_Storage.java       → DDClient_StorageImpl.java
-service/dingtalk/impl/DDImplClient_Workflow.java      → DDClient_WorkflowImpl.java
-```
-
-新接入子 client 时强行避开(R5 强制后缀)会让钉钉模块产生混搭风格,更难看,必须整改。
-
-但本 change 拆出 `standardize-client-service-layering` 独立处理,原因:
-
-- 14 文件改名 + Bean 名变化 + 跨仓 `@Qualifier` / `@Autowired by-name` 字面引用扫,爆炸半径大
-- R4 要求改前 grep 报告 + 用户 ACK;本 change 需要分批扫描
-- 失败回滚成本:一个文件改坏可能拖累钉钉栈所有调用方
-
-## What Changes
-
-按 R5 把 14 个中缀文件改为后缀风格。Spring DI by 接口的调用方不受影响(DDClient/DDClient_X 接口名不变);by-name 注入(如 `@Qualifier("dDImplClient_Attendance")`)需逐一找出替换。
-
-执行策略:**分批,每批 3~4 文件**,每批一个独立 commit:
-
-1. 批次 1:3 个 `DDImplClient` + `DDImplService` + `DDImplClient_Attendance`(主流量入口)
-2. 批次 2:通讯/通知 — `DDImplClient_Contacts` / `DDImplClient_Group` / `DDImplClient_Notice`
-3. 批次 3:考勤/人事 — `DDImplClient_Personnel` / `DDImplClient_Report` / `DDImplClient_Schedule`
-4. 批次 4:扩展 — `DDImplClient_Dedicated` / `DDImplClient_Event` / `DDImplClient_Extension` / `DDImplClient_Storage` / `DDImplClient_Workflow`
-
-每批落地前按 R4 流程跨仓 grep + 用户 ACK。
-
-## Capabilities
-
-### Modified Capabilities
-
-- `client-service-layering`:R5 从「新增代码强制」演进为「全仓覆盖」(钉钉存量符合后)
-
-### New Capabilities
-
-<!-- 无新能力,纯重构 -->
-
-## Impact
-
-- **重命名**:14 个 .java 文件
-- **改 import**:所有引用这 14 个具体 impl 类的代码(预计绝大多数是 interface 引用,少数 by-name)
-- **Spring Bean 名变化**:默认 Bean 名首字母小写,`dDImplClient_Attendance` → `dDClient_AttendanceImpl`;by-name 调用方需同步改
-- **跨仓影响**:
-  - mjava-ai 仓:自扫
-  - akds 项目:路径待用户确认;R4 grep
-  - 光明独立仓 `/Users/malk/server/cur/mjava-guangming/`:含 mjava-guangming/mjava-shunfeng 模块,可能引用
-  - 其他客户独立仓:用户在 ACK 时主动补
-- **运行时风险**:纯重命名,无逻辑变更;编译期能捕获所有问题
-
-## Non-Goals
-
-- ❌ 不改 `DDClient` / `DDClient_X` 接口名(仅改 Impl)
-- ❌ 不改方法签名(R4 触发的实质性变更不在本 change 范围)
-- ❌ 不顺手改其他模块(aliwork/beisen 已经是后缀风格,本 change 只动钉钉)
-- ❌ 不一次 commit 14 文件(必须分批,每批可独立回滚)

+ 0 - 44
openspec/changes/rename-dingtalk-impl-suffix/specs/client-service-layering/spec.md

@@ -1,44 +0,0 @@
-## MODIFIED Requirements
-
-### Requirement: R5 命名一致(Impl 后缀)
-
-> archive 后并入 `specs/client-service-layering/spec.md` 的 R5 段
-
-R5 演进:从「**新增代码强制后缀,存量钉钉中缀为待整改技术债**」演进为「**全仓所有实现类一律 `XxxImpl` 后缀**」。
-
-钉钉模块 14 个存量中缀文件已按本 change 分 4 批整改为后缀风格:
-
-| 旧 | 新 |
-|---|---|
-| `DDImplClient` | `DDClientImpl` |
-| `DDImplService` | `DDServiceImpl` |
-| `DDImplClient_Attendance` | `DDClient_AttendanceImpl` |
-| `DDImplClient_Contacts` | `DDClient_ContactsImpl` |
-| `DDImplClient_Dedicated` | `DDClient_DedicatedImpl` |
-| `DDImplClient_Event` | `DDClient_EventImpl` |
-| `DDImplClient_Extension` | `DDClient_ExtensionImpl` |
-| `DDImplClient_Group` | `DDClient_GroupImpl` |
-| `DDImplClient_Notice` | `DDClient_NoticeImpl` |
-| `DDImplClient_Personnel` | `DDClient_PersonnelImpl` |
-| `DDImplClient_Report` | `DDClient_ReportImpl` |
-| `DDImplClient_Schedule` | `DDClient_ScheduleImpl` |
-| `DDImplClient_Storage` | `DDClient_StorageImpl` |
-| `DDImplClient_Workflow` | `DDClient_WorkflowImpl` |
-
-R5 全文(更新后):
-
-所有实现类命名风格统一为 `XxxImpl` **后缀**:
-
-- 正:`DDClientImpl` / `DDClient_AttendanceImpl` / `DDServiceImpl` / `YDClient_FormImpl` / `BSServiceImpl`
-- 负:`DDImplClient` / `DDImplClient_Attendance` / `DDImplService`(中缀风格,全仓禁止)
-
-#### Scenario: 新增实现类(同 archive 前)
-
-- **WHEN** 给某 Client 接口新增实现
-- **THEN** 文件名必须用 `XxxImpl` 后缀,禁用 `XxxImplXxx` 中缀
-
-#### Scenario: 钉钉新增子 client 实现(更新)
-
-- **WHEN** 给 `service/dingtalk/impl/` 新增子 client 实现
-- **THEN** 必须用 `DDClient_XImpl` 后缀
-- **AND** 周围所有存量文件均已是后缀风格,无回退诱因

+ 0 - 52
openspec/changes/rename-dingtalk-impl-suffix/tasks.md

@@ -1,52 +0,0 @@
-## 0. 排期门控
-
-- [ ] 0.1 等 `standardize-client-service-layering` archive 入 specs/(R5 入规则)
-- [ ] 0.2 用户排期确认开始本 change
-
-## 1. 批次 1:主流量入口(3 文件)
-
-- [ ] 1.1 R4 grep 跨仓引用扫描(mjava-ai + 光明独立仓 + 用户指定的其他客户仓):
-  - `DDImplClient`(类名 / import / `@Qualifier` / `@Resource(name=)`)
-  - `DDImplService`
-  - `DDImplClient_Attendance`
-- [ ] 1.2 引用清单提交给用户,等 ACK
-- [ ] 1.3 改名:`DDImplClient.java` → `DDClientImpl.java`,class 声明同步
-- [ ] 1.4 改名:`DDImplService.java` → `DDServiceImpl.java`,class 声明同步
-- [ ] 1.5 改名:`DDImplClient_Attendance.java` → `DDClient_AttendanceImpl.java`,class 声明同步
-- [ ] 1.6 全仓改 import + `@Qualifier` + `@Resource(name=)` 引用
-- [ ] 1.7 `mvn -pl mjava -am clean compile` 通过
-- [ ] 1.8 `mvn -pl mjava-mcli,mjava-pro,mjava-com -am clean compile` 通过
-- [ ] 1.9 跨仓客户项目编译通过(用户指定)
-- [ ] 1.10 commit:`refactor(service/dingtalk): rename DDImpl* → DDImpl suffix [batch 1/4]`
-- [ ] 1.11 观察 1 周(或用户确认)→ 进入批次 2
-
-## 2. 批次 2:通讯通知(3 文件)
-
-- [ ] 2.1 R4 grep(Contacts / Group / Notice)
-- [ ] 2.2 ACK
-- [ ] 2.3 三个改名 + import 替换
-- [ ] 2.4 编译 + 跨仓验证
-- [ ] 2.5 commit `[batch 2/4]`
-
-## 3. 批次 3:人事日历(3 文件)
-
-- [ ] 3.1 R4 grep(Personnel / Report / Schedule)
-- [ ] 3.2 ACK
-- [ ] 3.3 三个改名 + import 替换
-- [ ] 3.4 编译 + 跨仓验证
-- [ ] 3.5 commit `[batch 3/4]`
-
-## 4. 批次 4:长尾(5 文件)
-
-- [ ] 4.1 R4 grep(Dedicated / Event / Extension / Storage / Workflow)
-- [ ] 4.2 ACK
-- [ ] 4.3 五个改名 + import 替换
-- [ ] 4.4 编译 + 跨仓验证
-- [ ] 4.5 commit `[batch 4/4]`
-
-## 5. 验证 & archive
-
-- [ ] 5.1 `grep -rE "DDImpl(Client|Service)" --include=\"*.java\"` 全仓零命中
-- [ ] 5.2 `/opsx:validate rename-dingtalk-impl-suffix --strict` 通过
-- [ ] 5.3 archive 到 `openspec/changes/archive/2026-XX-XX-rename-dingtalk-impl-suffix/`
-- [ ] 5.4 R5 在 specs/client-service-layering/spec.md 演进为「全仓覆盖」(删除"钉钉存量待整改"备注)

+ 0 - 91
openspec/changes/standardize-client-service-layering/design.md

@@ -1,91 +0,0 @@
-# Design
-
-## 关键决策
-
-### 1. 为什么用 capability spec 而不是只改 baseline
-
-`project-baseline.md` 现状是「指路 + 锚点 + 子项目清单」,纯结构指引。R1~R7 是**代码组织约束**(行为规则),属于 capability 层面。新建 `client-service-layering` capability spec 后:
-
-- baseline 只加一行锚点指向新 spec
-- 权威正文进共享 `mjava-baseline.md`(跨仓库引用)
-- 仓库内 spec 用 `## ADDED Requirements` 段落对齐 OpenSpec 模式
-
-这样 baseline 保持「薄」,规则改动走 capability spec 的 propose/archive 流程,演进路径清晰。
-
-### 2. R5 命名:为什么选 `Impl` 后缀
-
-仓内现状:
-- 后缀:宜搭 3 文件(YDClient_FormImpl/YDClient_ProcessImpl/YDClientImpl/YDServiceImpl)
-- 后缀:北森 4 文件(BSClient_AttendanceImpl/BSClient_EmployeeImpl/BSClientImpl/BSServiceImpl)
-- 后缀:其他 6 个产品所有 impl(EKB/FXK/CY/VK/XBB/TB/INTP/ALY 共 ~10 文件)
-- 中缀:钉钉 14 文件(DDImplClient + DDImplClient_X×12 + DDImplService)
-
-**14 文件中缀 vs ~17 文件后缀** —— 后缀是事实多数派。R5 选后缀,钉钉作为少数派改名,影响面已知(钉钉 14 文件 + `@Qualifier` 字面引用)。
-
-### 3. R7 准入条件:为什么是「≥2 个客户子项目复用」
-
-mjava 基座的价值是「**多客户复用的二次封装**」。单客户编排放基座会污染共享层:
-
-- 客户 A 的特殊错误码翻译进 `XxxService` → 客户 B 调用时被强加上
-- 客户 A 的特定字段映射进 Service → 客户 B 的字段被误转
-
-阈值定 2 是最低门槛:**第 2 个复用方出现 = 抽象有市场**。第 1 个复用方需要时,先在自己客户子项目里写编排;第 2 个想用时再上提到基座 Service。
-
-> 实操:第 1 个复用方写代码时,可以在子项目放在 `service/{product}/` 下用同样命名,后续上提时一份代码两边引用。
-
-### 4. R4 变更确认:grep 范围
-
-**当前阶段(基础建设期)**:mjava-ai 是纯基础建设项目,无实际客户子项目依赖本仓基座 jar,grep 范围 = **mjava-ai 本仓**。
-
-**客户接入后**:当某客户独立仓(如未来 akds / 光明独立仓 / 新客户)开始依赖 mjava-ai 发布的基座 jar 时,由用户在 ACK 流程中显式补充该仓库路径,扫描范围随之扩展。
-
-> 历史背景:光明独立仓 `/Users/malk/server/cur/mjava-guangming/` 自维护了一份 mjava 基座的分叉副本(独立 git 仓),不依赖 mjava-ai;akds 同样独立仓。这些客户仓的 R4 grep 由各自仓内自行执行,不与 mjava-ai 的 R4 流程联动。
-
-grep 模板:
-```bash
-# 接口签名
-grep -rE "\.{methodName}\(" --include="*.java" {repo}
-
-# 类名引用(迁移/重命名)
-grep -rE "(import|extends|implements)\s+.*\.{ClassName}" --include="*.java" {repo}
-```
-
-报告格式:
-```
-方法/类: com.malk.service.dingtalk.DDClient.foo
-mjava-ai: 3 处
-  - mjava-mcli/src/.../Boot.java:42
-  - mjava-pro/src/.../Xxx.java:88
-  - mjava/src/.../impl/DDImplClient.java:156(自定义,不计)
-akds: 1 处
-  - apps/web/src/.../Yyy.java:33
-光明独立仓: 0 处
-```
-
-### 5. O3 ALYConf 内容
-
-`ALYInvoiceImpl` 内 4 个 URL 字面量:
-
-```
-https://fapiao.market.alicloudapi.com/v2/invoice/pdf      → URL_INVOICE_PDF
-https://fapiao.market.alicloudapi.com/v2/invoice/query    → URL_INVOICE_QUERY
-https://fapiao.market.alicloudapi.com/v2/invoice/qrcode   → URL_INVOICE_QRCODE
-https://invoice.market.alicloudapi.com/v2/invoice/ocr     → URL_INVOICE_OCR
-```
-
-`ALYConf` 不用 `@ConfigurationProperties`(appcode 是调用方传参,不进 yml),只放 `public static final String` 常量。
-
-### 6. 为什么 O1 钉钉改名拆独立 change
-
-- 文件改动量大(14 文件 + Bean 名 + Qualifier 字面引用扫)
-- 跨仓引用扫范围至少 4 个仓库(mjava-ai/akds/光明/其他客户)
-- 失败回滚成本:1 个文件改坏可能拖累整个钉钉栈
-
-独立 change `rename-dingtalk-impl-suffix` 让改名风险隔离,可按文件批次推进。
-
-## 拒绝的方案
-
-- **直接进 mjava-baseline.md 不建仓库 spec** — 拒绝。OpenSpec 是仓内单一事实源,跨仓库共享文档需仓内 spec 作锚点
-- **R7 阈值定 1**(只要有 1 个复用就上提)— 拒绝。会鼓励过早抽象
-- **R5 选中缀** — 拒绝。事实多数派是后缀
-- **同时改名钉钉 12 文件** — 拒绝。爆炸半径过大,单独 change 隔离

+ 0 - 60
openspec/changes/standardize-client-service-layering/proposal.md

@@ -1,60 +0,0 @@
-> 状态(2026-06-10 立项):**提案阶段,规则文档 + O3/O4/O6 待 apply,O2 待 ACK**
-> 优先级:中。固化 7 条分层规则,配套 4 个轻量优化(O2/O3/O4/O6);O1(钉钉 impl 改名)拆到独立 change `rename-dingtalk-impl-suffix`。
-
-## Why
-
-`mjava` 基座现有 11 个产品接入板块(dingtalk / aliwork / beisen / aliyun / ekuaibao / fxiaoke / h3yun / integration / teambition / vika / xbongbong),整体已按产品分目录,但有 4 个不一致:
-
-1. **子 client 拆分粒度不均**:钉钉拆 12 子 client(按板块),宜搭/北森仅 2 子;EKB/FXK/CY/VK/XBB 单 Client 文件未拆 — 缺规则约定
-2. **`Impl` 命名风格分裂**:钉钉用中缀 `DDImplClient_X`,宜搭/北森用后缀 `XxxImpl` — 新人困惑
-3. **`util/` 与 `utils/` 并行**:2026-04-18 的 crypto 抽取留下中间态,`util/crypto/RSACrypt.java` 单独平行于 `utils/` 复数目录
-4. **`server/aliyun/` 缺 `ALYConf`**:`ALYInvoiceImpl` 内 4 个 URL 硬编码 — 与其他产品 server/ 结构不对齐
-5. **Service 层覆盖准入无规则**:5 个产品没 Service 层只暴露 Client,是否要补、什么时候补,没标准 → 容易写空壳
-
-更核心的痛点:**已有 Client/Service 接口签名改动时,跨仓库(mjava-ai / akds / 光明独立仓 / 其他客户仓)引用关系不可见**,改了下游一片报错。需要把"改前先扫引用 + 等用户 ACK"沉淀为强制规则。
-
-## What Changes
-
-新增 1 个 capability spec `client-service-layering`,固化 7 条规则:
-
-- **R1 原子接口分层**:`server/{product}/` 放产品方数据契约(POJO/Conf/枚举),`service/{product}/{Prod}Client*.java` 1:1 对应产品官方接口,`{Prod}Service.java` 做通用业务编排
-- **R2 板块拆 client**:单 Client > ~15 方法 或 跨 2+ 产品板块时,按板块拆 `{Prod}Client_{Module}.java`
-- **R3 调用优先级**:子项目调产品方接口必须先查 mjava `service/{product}/`;没有 → 按 R1/R2 新增;禁子项目自建 HTTP 直调
-- **R4 变更确认(核心)**:改 / 删 / 重命名任何 `*Client*.java` 或 `*Service*.java` 的**接口签名**前,必须 grep 全仓引用清单 → 报告 → 等用户 ACK;仅扩展(新方法/新子 client)不需要确认
-- **R5 命名一致**:所有实现类用 `XxxImpl` **后缀**风格(统一全仓,钉钉现有 12 个 `DDImplClient_X` 中缀产物拆到独立 change `rename-dingtalk-impl-suffix` 推进)
-- **R6 `server/` 层定位**:只放数据契约(POJO/Conf/枚举/常量),禁 `@Service` / `@Component`,禁注入,禁调 HTTP
-- **R7 Service 层准入**:仅当 ≥2 个客户子项目复用同一段二次编排时才建 `XxxService`;单客户编排留在客户子项目
-
-同步做 4 个轻量优化:
-
-- **O3** 补 `mjava/server/aliyun/ALYConf.java`,抽 `ALYInvoiceImpl` 内 4 个硬编码 URL 为常量(纯新增 + 一次性 import 替换,仅本仓引用,影响小)
-- **O4** INTP(集成平台)范式约定**仅写入文档**:未来扩多域时按 R1/R2 补 `INTPClient` 主入口与 `INTPService`(不做空壳预建 — 服从 R7)
-- **O6** 7 条规则进**权威**共享文档 `/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md`;仓库内 `openspec/specs/project-baseline.md` 加锚点
-- **O2** `util/crypto/RSACrypt.java` 迁到 `utils/crypto/RSACrypt.java`,删 `util/` 目录 — 按 R4 流程:本 change 仅做 grep 报告,**等用户 ACK 后再独立动**
-
-## Capabilities
-
-### New Capabilities
-
-- `client-service-layering`:mjava 仓 server/ + service/ 两层组织规则与变更流程
-
-### Modified Capabilities
-
-<!-- project-baseline.md 加锚点,不改语义 -->
-
-## Impact
-
-- **新增 spec**:`openspec/specs/client-service-layering/`(archive 后)
-- **新增代码**:`mjava/src/main/java/com/malk/server/aliyun/ALYConf.java`(一个常量类,~20 行)
-- **修改代码**:`mjava/src/main/java/com/malk/service/aliyun/impl/ALYInvoiceImpl.java` 4 处 URL 字面量改为 `ALYConf.URL_*` 引用 — 触发 R4 但作者即审核(本仓 + 本 change 范围内),grep 确认无下游 import URL 字面量后落地
-- **新增文档**:`/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md` 新章节"client/Service 分层规则"
-- **修改文档**:`openspec/specs/project-baseline.md` 加锚点;`README.md` 子项目速览表更新(如需)
-- **现有 Client/Service 代码**:零修改(除 ALYInvoiceImpl 4 行 URL 替换)
-- **客户子项目(mcli/pro/com 及独立仓 akds/光明)**:零影响
-
-## Non-Goals
-
-- ❌ 不改名钉钉 12 个 `DDImplClient_X`(拆到独立 change `rename-dingtalk-impl-suffix`,R5 仅约束新增代码)
-- ❌ 不立即迁 `util/crypto/RSACrypt.java`(O2 本 change 只 grep 报告,等 ACK 单独执行)
-- ❌ 不补 5 个产品(EKB/FXK/CY/VK/XBB)的 Service 层(R7 准入未达成)
-- ❌ 不强制现有 Client 单文件超过 15 方法立即拆分(R2 只约束新增;现有大文件作为技术债等下次触碰时顺手拆)

+ 0 - 196
openspec/changes/standardize-client-service-layering/specs/client-service-layering/spec.md

@@ -1,196 +0,0 @@
-## ADDED Requirements
-
-### Requirement: R1 原子接口分层(server / service 两层)
-
-mjava 基座对每个产品方接入按两层组织:
-
-- **`com.malk.server.{product}/`**:产品方原始数据契约。允许放 POJO(如 `*R` 响应、`*Param` 入参、`*Dto` 字段映射)、`{Prod}Conf` 配置类、枚举、常量、签名/加密工具类。禁业务逻辑。
-- **`com.malk.service.{product}/{Prod}Client*.java`**:1:1 对应产品官方接口文档,**一接口一方法**,不做组合,不做错误码翻译,仅做参数填充 + HTTP 调用 + 反序列化。
-- **`com.malk.service.{product}/{Prod}Service.java`**:可选。仅当存在跨客户共用的二次封装(自动续 token、分页拼装、错误码翻译、跨接口编排)时建立。准入条件见 R7。
-
-实现类一律落在 `com.malk.service.{product}.impl/` 子包。
-
-#### Scenario: 新增一个产品方接入
-
-- **WHEN** 需要在 mjava 基座新增对某产品方(如 "腾讯会议")的接入
-- **THEN** 必须建 `server/txmeeting/` 与 `service/txmeeting/` 两个目录
-- **AND** `server/txmeeting/` 下至少有 `TXMConf` 或 `TXMR` 至少一个数据契约类
-- **AND** `service/txmeeting/TXMClient.java` 接口 + `impl/TXMClientImpl.java` 实现各一个文件
-- **AND** 如无跨客户复用场景,**禁建** `TXMService.java` 空壳
-
-#### Scenario: Client 方法定义
-
-- **WHEN** 实现 `{Prod}Client*.java` 接口的方法
-- **THEN** 方法签名应与产品方官方接口文档 1:1 对应(每个 OpenAPI 一个 Java 方法)
-- **AND** 方法体只做:构造 headers / params / body → 调 `UtilHttp` → 反序列化 → 返回;**不调其他 Client 方法、不写 if-else 业务分支**
-
-#### Scenario: 集成平台 INTP 当前现状与演进
-
-- **WHEN** 当前 `service/integration/` 仅 `INTPClient_User`(单一域)
-- **THEN** **不**预建空壳 `INTPClient` 主入口或 `INTPService`(服从 R7 防空壳准入)
-- **AND** 未来扩多域时(例:补 `INTPClient_Org` / `INTPClient_Permission` 等)按 R2 触发:
-  - 若总方法数 > 15 或跨 2+ 域,必须建 `INTPClient` 主入口聚合
-  - 若 ≥2 客户子项目复用相同的二次编排(如统一鉴权续约),按 R7 准入条件补 `INTPService`
-
----
-
-### Requirement: R2 板块拆 client
-
-当单一 `{Prod}Client.java` 接口方法数 > 15,或跨 2+ 产品板块(例:钉钉的「通讯录」+「考勤」),必须按板块拆分为 `{Prod}Client_{Module}.java`。
-
-参照范式:
-- 钉钉 12 子 client:`DDClient_Attendance` / `DDClient_Contacts` / `DDClient_Group` 等
-- 宜搭 2 子 client:`YDClient_Form` / `YDClient_Process`
-- 北森 2 子 client:`BSClient_Attendance` / `BSClient_Employee`
-
-主 `{Prod}Client.java` 保留作为总入口或聚合(含 token 获取、通用工具方法),不消失。
-
-#### Scenario: Client 方法数超阈值
-
-- **WHEN** 给某 Client 增加新方法导致总数 > 15
-- **THEN** 必须在同次 PR 中拆出至少 1 个 `{Prod}Client_{Module}.java` 子文件,按业务板块归类
-- **OR** 如无法立即拆分,必须在 PR 描述里明确「**技术债**:超阈值 N 方法,跟踪 issue/change #xxx」
-
-#### Scenario: 跨板块新方法
-
-- **WHEN** 新方法所属产品板块(如「考勤」)与现有 Client 主体(如「通讯录」)不同
-- **THEN** 必须建新的 `{Prod}Client_{Module}.java` 而不是塞进现有 Client
-
----
-
-### Requirement: R3 调用优先级(子项目复用 mjava)
-
-所有 mjava-ai 仓内子项目(mcli / pro / com / 未来新建客户模块)和**外部独立客户仓**(akds / 光明独立仓 / 其他客户仓)调产品方接口时遵循优先级:
-
-1. **优先调** `mjava.service.{product}.{Prod}Client*` / `{Prod}Service`
-2. mjava 未提供但**确属产品方原子接口** → 按 R1/R2 在 mjava 新增 Client → 再被子项目调
-3. mjava 未提供且**仅为该客户独有业务编排**(非产品方原子接口)→ 留在客户子项目本地
-
-子项目**禁止**自建 HTTP 直调(绕过 `UtilHttp`)、禁止自建产品方鉴权/Token 逻辑(绕过 `UtilToken`)、禁止引入产品方三方 SDK。
-
-#### Scenario: 子项目发现 mjava 缺接口
-
-- **WHEN** 客户子项目需要某产品方接口,mjava 当前未提供
-- **THEN** 先判断该接口是否为「产品方原子能力」
-- **AND** 是 → 起 mjava 侧 change 新增 Client → mjava 发版 → 子项目升级 mjava 版本调用
-- **AND** 否(客户独有编排)→ 在子项目本地写,不进 mjava
-
-#### Scenario: 子项目存量 HTTP 直调
-
-- **WHEN** 巡检发现客户子项目内有 `UtilHttp.doXxx` 直接调产品方域名
-- **THEN** 标记为待迁移技术债
-- **AND** 下次触碰该代码段时按 R3 上提到 mjava Client
-
----
-
-### Requirement: R4 变更确认(核心)
-
-对 `com.malk.service.{product}.{Prod}Client*.java` 或 `{Prod}Service.java` 的**接口签名**做以下任一变更前,必须执行变更确认流程:
-
-- 删除方法
-- 改名方法
-- 改方法参数列表(增/减/换类型/换顺序)
-- 改方法返回类型
-- 删除/改名接口本身
-- 删除/改名 `server/` 层 POJO 类、字段、方法
-
-变更确认流程:
-
-1. **grep 引用扫描**:
-   - **基础建设阶段(当前默认)**:扫 mjava-ai 本仓全量
-   - **有客户子项目依赖时**:用户在 ACK 流程中显式列出需扩扫的下游仓库路径(如未来 akds / 光明独立仓 / 新客户仓)
-2. **生成引用清单**:file:line + 调用上下文
-3. **报告给用户**:影响面 + 拟改动方案 + 兼容路径(如有)
-4. **等待用户 ACK**:用户回复"OK / 同意 / 改吧"等明确肯定后方可执行
-5. **变更后回写**:commit message 注明 `BREAKING: {Class}.{method} 签名变更`,受影响调用方列表写入 commit body
-
-**仅扩展**变更(新方法 / 新重载 / 新子 client 文件 / 新 server 层 POJO)**不需要**确认,但 commit message 注明 `feat(service): 新增 {Class}.{method}`。
-
-#### Scenario: 改 Client 方法签名
-
-- **WHEN** 计划修改 `DDClient_Contacts.getUserDetail(String userid)` 改为 `getUserDetail(String userid, Boolean includeExt)`
-- **THEN** 必须先执行 grep `\.getUserDetail\(` 跨仓扫描
-- **AND** 生成引用清单提交给用户
-- **AND** 在用户 ACK 前**不修改源代码**
-- **AND** ACK 后才动手,commit message 标 `BREAKING`
-
-#### Scenario: 仅扩展(加新方法)
-
-- **WHEN** 给 `DDClient_Contacts` 新增 `listDepartmentTree()` 方法
-- **THEN** 直接实现,无需扫引用
-- **AND** commit message 写 `feat(service/dingtalk): DDClient_Contacts 新增 listDepartmentTree`
-
----
-
-### Requirement: R5 命名一致(Impl 后缀)
-
-所有实现类命名风格统一为 `XxxImpl` **后缀**:
-
-- 正:`DDClientImpl` / `DDClient_AttendanceImpl` / `DDServiceImpl` / `YDClient_FormImpl` / `BSServiceImpl`
-- 负:`DDImplClient` / `DDImplClient_Attendance` / `DDImplService`(中缀风格,禁止新增)
-
-R5 对**新增代码**强制生效。钉钉现有 14 个中缀文件(`DDImplClient` + `DDImplClient_X×12` + `DDImplService`)作为存量技术债,拆到独立 change `rename-dingtalk-impl-suffix` 推进,本 change 不动。
-
-#### Scenario: 新增实现类
-
-- **WHEN** 给某 Client 接口新增实现
-- **THEN** 文件名必须用 `XxxImpl` 后缀,禁用 `XxxImplXxx` 中缀
-
-#### Scenario: 钉钉新增子 client 实现
-
-- **WHEN** 给 `service/dingtalk/impl/` 新增子 client 实现
-- **THEN** 即使周围 12 个文件是 `DDImplClient_X` 中缀风格,新文件必须用 `DDClient_XImpl` 后缀
-- **AND** 不为「对齐周围风格」回退 R5
-
----
-
-### Requirement: R6 server/ 层定位(数据契约纯净性)
-
-`com.malk.server.{product}/` 包内的类必须满足:
-
-- **允许**:POJO(含 `*R` `*Param` `*Dto` `*Conf`)、枚举(`enum`)、常量类(`public static final`)、产品方签名/加密工具类(如 `DingCallbackCrypto`、`DigestUtil`)
-- **禁止**:
-  - `@Service` / `@Component` / `@Repository` / `@RestController` 等 Bean 注解
-  - `@Autowired` / `@Resource` / 构造器注入其他 Bean
-  - 直接调 `UtilHttp` / `UtilToken` 发起网络请求
-  - 含业务编排逻辑(if-else 业务分支、跨产品组合)
-
-`{Prod}Conf` 是 `@ConfigurationProperties` 的允许例外(标了 `@Component` 注解但仅为配置绑定,不参与业务)。
-
-#### Scenario: 误把业务类放进 server/
-
-- **WHEN** 代码评审发现 `server/dingtalk/` 下出现 `@Service` 注解的类
-- **THEN** 必须移到 `service/dingtalk/impl/` 或客户子项目对应位置
-
-#### Scenario: server/ 类的依赖
-
-- **WHEN** 写 `server/{product}/` 下的 POJO 类
-- **THEN** import 范围限定在:JDK / Lombok / fastjson / `com.malk.server.common.*` / 同包内类
-- **AND** 不得 import `com.malk.service.*` / `com.malk.utils.*`(除常量工具如 `UtilMap` 静态方法)
-
----
-
-### Requirement: R7 Service 层准入(防空壳)
-
-`{Prod}Service.java` 仅当满足以下任一条件时才建:
-
-- 当前有 ≥2 个客户子项目(包括 mjava-ai 内 mcli/pro/com 与外部独立客户仓)使用相同二次编排逻辑
-- 该编排涉及非业务的通用横切(自动续 token、统一分页拼装、产品方错误码 → `McException` 翻译)
-- 该编排为新接入产品的「试水期占位」,提案中明确承诺 3 个月内补复用方,否则归零
-
-不满足上述条件时,二次编排留在客户子项目内 `com.malk.{customer}.service.{product}.{Prod}LocalService.java`,命名带 `Local` 前缀以示区分。
-
-未满足条件强行预建 Service 文件(空 interface / 仅一个 default 方法 / 仅一个未被任何客户调用的方法)视为违反 R7,code review 必须打回。
-
-#### Scenario: 新接入产品评估 Service 层
-
-- **WHEN** 在 mjava 接入新产品(如「腾讯会议」)
-- **THEN** 默认**只建 Client**,不建 Service
-- **AND** 第一个客户的二次编排放在客户子项目本地
-- **AND** 第二个客户来要复用时再上提
-
-#### Scenario: 误建空壳 Service
-
-- **WHEN** PR 中出现 `XxxService.java` 接口仅有 1~2 个未被任何子项目调用的方法
-- **THEN** code review 必须打回
-- **AND** 编排迁回客户子项目

+ 0 - 44
openspec/changes/standardize-client-service-layering/tasks.md

@@ -1,44 +0,0 @@
-## 1. 规则文档
-
-- [ ] 1.1 共享权威文档 `/Users/malk/Desktop/Tech/claude/后端/mjava-baseline.md` 新增章节 "client/Service 分层规则",正文为 R1~R7 七条
-- [ ] 1.2 仓库内 `openspec/specs/project-baseline.md` 在"代码锚点"段后加锚点:`> 分层规则见 capability spec [client-service-layering](changes/standardize-client-service-layering/specs/client-service-layering/spec.md)(archive 后路径变更)`
-- [ ] 1.3 `README.md` 子项目速览或基座章节加一行:分层规则锚点(同上)
-
-## 2. O3 补 ALYConf 并替换硬编码 URL
-
-- [ ] 2.1 新建 `mjava/src/main/java/com/malk/server/aliyun/ALYConf.java`,4 个 `public static final String` 常量:
-  - `URL_INVOICE_PDF = "https://fapiao.market.alicloudapi.com/v2/invoice/pdf"`
-  - `URL_INVOICE_QUERY = "https://fapiao.market.alicloudapi.com/v2/invoice/query"`
-  - `URL_INVOICE_QRCODE = "https://fapiao.market.alicloudapi.com/v2/invoice/qrcode"`
-  - `URL_INVOICE_OCR = "https://invoice.market.alicloudapi.com/v2/invoice/ocr"`
-- [ ] 2.2 改 `mjava/src/main/java/com/malk/service/aliyun/impl/ALYInvoiceImpl.java`:4 处 URL 字面量替换为 `ALYConf.URL_*` 引用
-- [ ] 2.3 grep 跨仓 `fapiao.market.alicloudapi.com` 和 `invoice.market.alicloudapi.com` 字面量,确认无其他副本(按 R4 流程)
-- [ ] 2.4 `mvn -pl mjava -am compile` 通过
-
-## 3. O4 INTP 范式约定(仅文档)
-
-- [ ] 3.1 在 capability spec `client-service-layering` 的 R7 Scenario 区,明确「集成平台 INTP 当前仅 `INTPClient_User`,未来扩多域时按 R1/R2 补 `INTPClient` 主入口;按 R7 评估后再补 `INTPService`」
-- [ ] 3.2 不动 `service/integration/` 任何代码
-
-## 4. O6 规则进 baseline(与 1.1/1.2 合并执行)
-
-- [ ] 4.1 已在 1.1/1.2 覆盖,本节为占位标记,验收 1.1/1.2 完成即可
-
-## 5. O2 RSACrypt 迁移(仅 grep 报告,等 ACK)
-
-- [ ] 5.1 grep `import com\.malk\.util\.crypto\.RSACrypt` 全仓 + 跨仓(mjava-ai / akds / 光明独立仓 / 其他用户在 ACK 时主动补的仓)
-- [ ] 5.2 输出引用清单(file:line + 调用上下文)给用户
-- [ ] 5.3 **等待用户 ACK**:用户回复"OK 迁"才执行下一步;否则本任务长期挂起
-- [ ] 5.4 ACK 后:复制 `util/crypto/RSACrypt.java` 到 `utils/crypto/RSACrypt.java`
-- [ ] 5.5 全仓改 import `com.malk.util.crypto.RSACrypt` → `com.malk.utils.crypto.RSACrypt`
-- [ ] 5.6 删 `util/crypto/RSACrypt.java` 和空目录 `util/`
-- [ ] 5.7 `mvn -pl mjava -am compile` 通过
-- [ ] 5.8 commit message 标 `refactor(util→utils): RSACrypt 迁移`,body 列受影响 import
-
-## 6. 验证
-
-- [ ] 6.1 `mvn -pl mjava -am compile` 全部通过
-- [ ] 6.2 子项目(mcli/pro/com)`mvn compile` 全部通过
-- [ ] 6.3 grep 验证:`server/` 下无 `@Service` `@Component`(R6 除 `*Conf`)
-- [ ] 6.4 grep 验证:spec 7 条规则在 mjava-baseline.md / project-baseline.md / spec.md 三处一致
-- [ ] 6.5 `/opsx:validate standardize-client-service-layering --strict` 通过

+ 0 - 22
openspec/specs/crypto-utils/spec.md

@@ -1,22 +0,0 @@
-# crypto-utils Specification
-
-## Purpose
-TBD - created by archiving change extract-dingtalk-standard-api. Update Purpose after archive.
-## Requirements
-### Requirement: RSACrypt 下沉到基座
-
-系统 SHALL 把现有 `com.malk.guangming.util.RSACrypt` 完整复制到基座 `com.malk.util.crypto.RSACrypt`,保持方法签名与行为完全一致(`encrypt(str, publicKey)` + `toUrlSafe(base64)`)。本次**不新增**任何方法。
-
-#### Scenario: 基座新位置可用
-- **WHEN** 任一业务模块 import `com.malk.util.crypto.RSACrypt`
-- **THEN** 调用 `encrypt`、`toUrlSafe` 结果与旧 `com.malk.guangming.util.RSACrypt` 逐字节一致
-
-### Requirement: 旧位置保留 Deprecated 壳
-
-系统 SHALL 在 `com.malk.guangming.util.RSACrypt` 保留类名与方法签名,内部委托到新类并加 `@Deprecated`,避免破坏已编译的业务代码。
-
-#### Scenario: 旧代码继续编译运行
-- **WHEN** 业务代码仍写 `com.malk.guangming.util.RSACrypt.encrypt(...)`
-- **THEN** 编译通过;运行结果与调用新类完全一致
-- **AND** 首次调用打印一次 WARN 日志引导迁移(可选,不阻塞功能)
-

+ 0 - 99
openspec/specs/dingtalk-contacts-v2/spec.md

@@ -1,99 +0,0 @@
-# dingtalk-contacts-v2 Specification
-
-## Purpose
-TBD - created by archiving change extend-dingtalk-contacts-api. Update Purpose after archive.
-## Requirements
-### Requirement: 用户管理完整覆盖
-
-`DDClient_Contacts` SHALL 提供对钉钉官方通讯录用户管理 endpoint 的完整对齐。所有方法 MUST 遵守 `mjava-baseline §3.4.2` 的签名规则。
-
-#### Scenario: 创建用户(完整字段)
-
-- **WHEN** 调用 `createUser_v2(access_token, name, mobile, dept_id_list, body_ext)`
-- **THEN** `body_ext` 必须支持官方文档列出的所有可选字段(userid / hired_date / job_number / title / email / senior_mode / extension / ...)
-- **AND** javadoc 必须枚举这些字段并注明类型
-
-#### Scenario: 更新用户
-
-- **WHEN** 调用 `updateUser(access_token, userid, body_ext)`
-- **THEN** `userid` 必填,其他字段全部通过 `body_ext` 传入
-- **AND** 不传的字段保持钉钉侧原值不变
-
-#### Scenario: 按 unionId 查询
-
-- **WHEN** 调用 `getUserByUnionId(access_token, union_id)`
-- **THEN** 返回 userid(二次调用 `getUser_v2` 获取详情)
-
-#### Scenario: 查询管理员
-
-- **WHEN** 调用 `listAdmins(access_token)`
-- **THEN** 返回管理员 userid 列表,含主管理员与子管理员区分字段
-
-#### Scenario: 查询未激活员工
-
-- **WHEN** 调用 `listInactiveUsers(access_token, is_active, offset, size, body_ext)`
-- **THEN** 支持按激活状态过滤
-
-### Requirement: 部门管理完整覆盖
-
-`DDClient_Contacts` SHALL 提供部门 CRUD 与层级查询的完整对齐。所有方法 MUST 对应钉钉官方部门管理 endpoint。
-
-#### Scenario: 部门 CRUD
-
-- **WHEN** 依次调用 `createDepartment_v2` / `updateDepartment` / `deleteDepartment` / `getDepartment_v2`
-- **THEN** 每个方法参数完整对齐官方文档
-- **AND** `body_ext` 支持 `hide_dept` / `dept_permits` / `user_permits` / `outer_dept` / `source_identifier` 等全部可选项
-
-#### Scenario: 父部门链查询
-
-- **WHEN** 调用 `listParentByDept(access_token, dept_id)`
-- **THEN** 返回目标部门到根部门的父链
-
-### Requirement: 角色管理
-
-`DDClient_Contacts` SHALL 提供角色的增删改查与批量员工授权能力。
-
-#### Scenario: 新增角色
-
-- **WHEN** 调用 `addRole(access_token, roleName, groupId)`
-- **THEN** 返回 roleId
-
-#### Scenario: 批量分配角色
-
-- **WHEN** 调用 `addRolesForEmps(access_token, roleIds, userIds)`
-- **THEN** 一次调用同时为多员工授权多角色
-
-#### Scenario: 查询角色成员
-
-- **WHEN** 调用 `listRoleEmployees(access_token, role_id, size, offset)`
-- **THEN** 分页返回角色下员工列表
-
-### Requirement: 员工字段可见性
-
-`DDClient_Contacts` SHALL 提供员工档案字段隐藏规则的管理能力。
-
-#### Scenario: 隐藏字段设置
-
-- **WHEN** 调用 `upsertHideField(access_token, name, field, userIds, deptIds)`
-- **THEN** 对指定员工或部门隐藏指定员工档案字段
-
-#### Scenario: 查询隐藏字段
-
-- **WHEN** 调用 `listHideFields(access_token, size, offset)`
-- **THEN** 返回企业当前生效的字段隐藏规则列表
-
-### Requirement: 参数透传约束
-
-本 capability 所有方法 MUST 满足官方可选参数完整透传与文档化要求,不得因 Java 侧建模省事而过滤字段。
-
-#### Scenario: body_ext 不得过滤官方可选参数
-
-- **WHEN** 调用方通过 `body_ext` 传入官方文档支持的任意可选字段
-- **THEN** 实现必须原样透传到 HTTP 请求体,不得删除或改写
-- **AND** 方法 javadoc 必须列出所有已知 body_ext key 并注明类型
-
-#### Scenario: 方法必须有 apiNote
-
-- **WHEN** 新增任一 Client 方法
-- **THEN** javadoc 必须含 `@apiNote` 链到该 endpoint 的钉钉官方文档页
-

+ 0 - 85
openspec/specs/integration-user-api/spec.md

@@ -1,85 +0,0 @@
-## ADDED Requirements
-
-### Requirement: 集成平台 OAuth2 鉴权与 token 缓存
-
-`INTPImplClient_User#getAccessToken()` SHALL 通过 `client_credentials` 流程获取 access_token,并使用 `UtilToken` 缓存。CRUD 方法 MUST 接收 `access_token` 作为第一个参数,调用方先调 `getAccessToken()` 再传入。
-
-#### Scenario: 首次拉取 access_token
-
-- **WHEN** `UtilToken` 缓存中无 `integration:accessToken`
-- **THEN** 向 `{baseUrl}/iam/token` 发 POST,body 为 `application/x-www-form-urlencoded`,字段:`grant_type=client_credentials` / `client_id={IntpConf.clientId}` / `client_secret={IntpConf.clientSecret}`
-- **AND** 响应 `access_token` 写入缓存,TTL = `expires_in - 60` 秒
-
-#### Scenario: 缓存命中
-
-- **WHEN** `UtilToken` 缓存中已有未过期的 `integration:accessToken`
-- **THEN** 直接复用,不发 HTTP 请求
-
-#### Scenario: token 失效
-
-- **WHEN** 服务端 revoke 或过期
-- **THEN** 调用方收到 HTTP 401 + `error/error_description`
-- **AND** 本期不做"401 自动重拿",由调用方决定重试策略
-
-### Requirement: 用户创建
-
-`INTPClient_User.createUser` SHALL 对齐 apifox 接口 `POST /iam/api/users`。必填字段必须显式入参;可选字段通过 `body_ext` Map 注入。
-
-#### Scenario: 创建用户(最小集)
-
-- **WHEN** 调用 `createUser(accessToken, "alice", "Pwd@123", null)`
-- **THEN** 向 `{baseUrl}/iam/api/users` 发 POST `application/json`
-- **AND** Header `Authorization: Bearer {accessToken}`
-- **AND** body 为 `{"username":"alice","password":"Pwd@123"}`
-
-#### Scenario: 创建用户(完整字段)
-
-- **WHEN** `body_ext` 含 `name` / `email` / `phone_number` / `user_job_number` / `nick_name` / `picture` / `org_ids` / `address` / `title` / `hired_date` / `tag` / `group_positions`
-- **THEN** body 合并 `username` / `password` 与 `body_ext` 全部字段
-- **AND** javadoc MUST 枚举上述字段名 + 类型 + 含义
-
-#### Scenario: 创建失败
-
-- **WHEN** 服务端返回 `{"result":false,"error":"USER_EXIST","error_description":"用户已存在"}`
-- **THEN** 返回 `McR.error("USER_EXIST", "用户已存在")`
-
-### Requirement: 用户修改
-
-`INTPClient_User.updateUser` SHALL 对齐 apifox 接口 `PATCH /iam/api/user`。`username` 通过 Query Param 传递(**非 Path**),其它字段全部走 body。
-
-#### Scenario: 修改单字段
-
-- **WHEN** 调用 `updateUser(accessToken, "alice", Map.of("email", "alice@new.com"))`
-- **THEN** 向 `{baseUrl}/iam/api/user?username=alice` 发 PATCH
-- **AND** body 为 `{"email":"alice@new.com"}`
-- **AND** 不传的字段保持服务端原值不变
-
-### Requirement: 用户删除(批量)
-
-`INTPClient_User.deleteUsers` SHALL 对齐 apifox 接口 `POST /iam/api/users/delete`。**HTTP 方法是 POST**,因 body 需带 username 数组。
-
-#### Scenario: 批量按登录名删除
-
-- **WHEN** 调用 `deleteUsers(accessToken, List.of("alice","bob"))`
-- **THEN** 向 `{baseUrl}/iam/api/users/delete` 发 POST `application/json`
-- **AND** body 为 `{"usernames":["alice","bob"]}`
-
-### Requirement: 用户查询
-
-`INTPClient_User.queryUsers` SHALL 对齐 apifox 接口 `GET /iam/api/users`,支持分页与多维过滤。
-
-#### Scenario: 关键字搜索
-
-- **WHEN** 调用 `queryUsers(accessToken, Map.of("q","alice","page",1,"size",20))`
-- **THEN** 向 `{baseUrl}/iam/api/users?q=alice&page=1&size=20` 发 GET
-- **AND** 解析响应 `data.items[]` 返回 `McR.success(data)`
-
-### Requirement: 配置段独立命名
-
-`INTPConf` SHALL 通过 Spring `@ConfigurationProperties("integration")` 加载配置;`baseUrl` 不含 `/iam` 前缀。
-
-#### Scenario: 三个客户子模块的 yml 模板
-
-- **WHEN** 浏览 `mjava-mcli` / `mjava-shunfeng` / `mjava-guangming` 的 `application-{dev,prod}.yml.example`
-- **THEN** 每个模板 MUST 含 `integration:` 段示例,字段:`baseUrl` / `clientId` / `clientSecret`
-- **AND** 真实凭据从 `${INTP_CLIENT_ID}` / `${INTP_CLIENT_SECRET}` 环境变量注入

+ 0 - 58
openspec/specs/project-baseline.md

@@ -1,58 +0,0 @@
-# project-baseline
-
-> mjava-ai 仓库稳态基线。2026-04-18 建立。
-
-## 权威规范位置
-
-**不在本文件**。规范正文维护在跨仓库共享的文档中心:
-
-- 通用后端基座:`/Users/malk/Desktop/Tech/claude/后端/CLAUDE.md`(含 Client/Service 分层规则 R1~R7)
-- 宜搭服务端特化:`/Users/malk/Desktop/Tech/claude/后端/.claude/docs/yida-serverside.md`
-- 顶层共享规范:`/Users/malk/Desktop/Tech/claude/CLAUDE.md`
-- 分层规则 capability spec:`openspec/specs/client-service-layering/spec.md`(archive 后;当前在 `openspec/changes/standardize-client-service-layering/specs/`)
-
-本文件只做三件事:**指路、列代码锚点、列子项目清单**。
-
-## 代码锚点
-
-基座 `mjava/src/main/java/com/malk/`:
-
-| 能力 | 路径 |
-|------|------|
-| HTTP 请求封装 | `utils/UtilHttp.java` |
-| Token 缓存 | `utils/UtilToken.java` |
-| 统一响应 | `server/common/McR.java`、`server/common/VenR.java` |
-| 业务异常 | `server/common/McException.java` |
-| 全局异常拦截 | `filter/CatchException.java` |
-| 链路追踪 | `filter/TraceIdFilter.java` |
-| 请求拦截器 | `filter/RequestInterceptor.java` |
-| Repository 基类 | `base/BaseRepository.java` |
-| PO 基类(含审计) | `base/BasePo.java` |
-| 多数据源配置 | `config/DataSourceConfig.java` |
-| 启动类 | `Boot.java`(含 `JPAQueryFactory` Bean) |
-
-第三方对接服务:`service/{dingtalk,aliwork,beisen,ekuaibao,fxiaoke,h3yun,teambition,vika,xbongbong}/`
-
-## 子项目清单
-
-| 模块 | 作用 | 备注 |
-|------|------|------|
-| `mjava` | 基座 jar | 无 repackage,被子项目 `${mjava.version}` 依赖 |
-| `mjava-mcli` | 客户模板 | 最小化 Boot.java + yml;新客户接入复制此模块 |
-| `mjava-pro` | 平台增强 | 多租户/动态 DDL 等高级能力(建设中) |
-| `mjava-com` | 通用 action | 钉钉/宜搭 action 注册中心(建设中) |
-
-## 新客户接入(简版)
-
-1. `cp -r mjava-mcli mjava-{customer}`
-2. 改 `pom.xml` 的 `artifactId`
-3. 改 `Boot.java` 包名与 `scanBasePackages = {"com.malk"}` 保持
-4. 改 `application-{dev,prod}.yml` 的 `server.port` / `server.servlet.context-path` / `spring.datasource.*`
-5. 根 `pom.xml` `<modules>` 追加 `<module>mjava-{customer}</module>`
-6. `mvn -pl mjava-{customer} -am clean compile` 验证
-
-完整流程与约束见权威文档第 9 章。
-
-## 演进
-
-对本基线的任何修改(新增代码约定、调整包结构、引入新依赖),必须走 `/opsx:propose` 流程新建 change,经 `apply` 后 `archive`。**不直接编辑本文件**。

+ 0 - 62
openspec/specs/replay-guard/spec.md

@@ -1,62 +0,0 @@
-# replay-guard Specification
-
-## Purpose
-TBD - created by archiving change add-request-auth-replay-guard. Update Purpose after archive.
-## Requirements
-### Requirement: 时间窗校验
-
-mjava 基座 SHALL 拒绝时间戳超出配置窗口的请求,以限制重放攻击的有效窗口。
-
-#### Scenario: 时间戳在窗口内
-
-- **WHEN** `X-MJ-Timestamp` 与服务端当前时间差 ≤ `mjava.auth.window`(默认 300 秒)
-- **THEN** 进入后续 Nonce 与签名校验
-
-#### Scenario: 时间戳超窗
-
-- **WHEN** 时间差 > `window`(无论提前还是滞后)
-- **THEN** 返回 `401 { code: "AUTH_TIMESTAMP_OUT_OF_WINDOW" }`
-- **AND** 审计日志记录 `clientTimestamp` 与 `serverTimestamp` 供排查
-
-### Requirement: Nonce 去重
-
-mjava 基座 SHALL 通过 Nonce 缓存实现在时间窗内的一次性请求保证。同一 Nonce 在窗口期内只允许通过一次。
-
-#### Scenario: 首次 Nonce 被接受
-
-- **WHEN** 请求携带之前未见过的 `X-MJ-Nonce`
-- **THEN** `NonceCache.putIfAbsent(nonce)` 成功
-- **AND** 请求进入签名校验
-
-#### Scenario: Nonce 被重放
-
-- **WHEN** 请求携带之前已见过的 Nonce(仍在 TTL 内)
-- **THEN** `NonceCache.putIfAbsent` 返回 false
-- **AND** 返回 `401 { code: "AUTH_NONCE_REPLAYED" }`
-
-#### Scenario: Nonce 缓存容量上限
-
-- **WHEN** `NonceCache` 达到 `mjava.auth.nonce-cache-size` 上限
-- **THEN** 按 LRU 策略淘汰最旧条目
-- **AND** 淘汰不影响未淘汰 Nonce 的判定正确性
-
-### Requirement: Nonce 缓存 TTL 设置
-
-`NonceCache` 的 TTL MUST 略长于 `mjava.auth.window`,以避免边界时间戳被错误接受。
-
-#### Scenario: TTL 等于窗口 + 30s
-
-- **WHEN** NonceCache 初始化
-- **THEN** TTL = `mjava.auth.window + 30` 秒
-- **AND** 例:window=300 → TTL=330
-
-### Requirement: 单实例内存限制声明
-
-Phase 1 的 Nonce 去重 SHALL 在单 JVM 实例内生效;多实例部署时同一 Nonce 可能被两个实例各接受一次。这是已知限制,不在本 change 范围。
-
-#### Scenario: 多实例部署告警
-
-- **WHEN** 生产部署判定需要多实例横向扩展
-- **THEN** 运维必须评估是否升级为分布式 Nonce(Redis),独立走新 change 提案
-- **AND** 本 change 的 NonceCache 配置 `nonce-cache-size` 建议单实例足够承载高峰 QPS × window 秒的组合
-

+ 0 - 66
openspec/specs/request-auth/spec.md

@@ -1,66 +0,0 @@
-# request-auth Specification
-
-## Purpose
-TBD - created by archiving change add-request-auth-replay-guard. Update Purpose after archive.
-## Requirements
-### Requirement: HMAC 签名校验
-
-mjava 基座 SHALL 提供可配置的 HMAC-SHA256 请求签名校验机制。当 `mjava.auth.enabled=true` 时,所有非豁免请求 MUST 通过签名校验才能进入业务 Controller。
-
-#### Scenario: 合法签名请求通过
-
-- **WHEN** 请求携带完整四个 Header(`X-MJ-Key` / `X-MJ-Timestamp` / `X-MJ-Nonce` / `X-MJ-Signature`)
-- **AND** 签名 = `HMAC-SHA256(secret, timestamp + "\n" + nonce + "\n" + method + "\n" + path + "\n" + bodyHash)`
-- **THEN** 请求被放行到 Controller
-- **AND** MDC 写入 `authKey` 字段供日志追溯
-
-#### Scenario: 签名不匹配
-
-- **WHEN** 计算出的签名与 Header 中 `X-MJ-Signature` 不一致
-- **THEN** 返回 `403 { code: "AUTH_SIGNATURE_INVALID" }`
-- **AND** 响应 message 不包含期望签名值(避免泄露线索)
-- **AND** 审计日志记录 `authKey + path + latencyMs`
-
-#### Scenario: Header 缺失
-
-- **WHEN** 四个必备 Header 中任一缺失
-- **THEN** 返回 `401 { code: "AUTH_HEADER_MISSING" }`
-- **AND** 响应不说明具体缺失哪个
-
-### Requirement: 豁免机制
-
-本能力 SHALL 支持三级豁免策略,优先级从上到下:全局开关 > 路径豁免 > 注解豁免。
-
-#### Scenario: 全局关闭
-
-- **WHEN** `mjava.auth.enabled=false`
-- **THEN** 所有请求跳过鉴权,行为等同于 enable 前的裸奔状态
-
-#### Scenario: 路径豁免
-
-- **WHEN** 请求路径匹配 `mjava.auth.exempt-paths` 列表中任一 Ant 风格模式
-- **THEN** 跳过签名校验
-- **AND** 典型清单:`/actuator/**`(健康检查)、`/api/*/callback/**`(第三方 webhook)
-
-#### Scenario: 注解豁免
-
-- **WHEN** 目标 Controller 方法或其类带 `@NoAuth` 注解
-- **THEN** 签名校验跳过
-- **AND** 注解识别发生在 `HandlerInterceptor.preHandle`(能访问到 `HandlerMethod`)
-
-### Requirement: 密钥配置
-
-`secret` MUST 通过环境变量注入,禁止硬编码。`enabled=true` 但 `secret` 为空时 MUST 拒绝启动或拒绝请求。
-
-#### Scenario: secret 未配置
-
-- **WHEN** `mjava.auth.enabled=true` 但 `mjava.auth.secret` 为空或未设置
-- **THEN** 启动时打 ERROR 日志
-- **AND** 所有受保护请求返回 `503 { code: "AUTH_CONFIG_MISSING" }`
-
-#### Scenario: secret 从环境变量读取
-
-- **WHEN** `application.yml` 中 `secret: ${AUTH_SECRET}`
-- **AND** 部署环境设置 `AUTH_SECRET` 变量
-- **THEN** 启动时成功加载,后续请求按此密钥校验
-

+ 0 - 72
openspec/specs/yida-form-atomic/spec.md

@@ -1,72 +0,0 @@
-# yida-form-atomic Specification
-
-## Purpose
-TBD - created by archiving change extend-yida-api-coverage. Update Purpose after archive.
-## Requirements
-### Requirement: 表单实例 CRUD 原子接口
-
-`YDClient_Form` SHALL 提供与宜搭官方 1:1 对应的表单实例增删改查原子方法。方法签名 MUST 严格遵守 `mjava-baseline §3.4.2`(必填参数显式 + `body_ext` 承接可选参数 + javadoc 枚举全部 body_ext key + `@apiNote` 链到官方文档)。
-
-#### Scenario: 新增表单实例
-
-- **WHEN** 调用 `YDClient_Form.saveForm(conf, formUuid, formDataJson, body_ext)`
-- **THEN** 发起 `POST /v1.0/yida/forms/instances`(或旧版 `/dingtalk/yida/processes/saveFormData`)
-- **AND** `body_ext` 所有可选参数必须透传(不删、不填默认)
-- **AND** 失败时抛 `McException` 带宜搭原始错误码
-
-#### Scenario: 更新表单实例
-
-- **WHEN** 调用 `YDClient_Form.updateForm(conf, formInstanceId, updateFormDataJson, body_ext)`
-- **THEN** 必须支持 `useLatestVersion` / `ignoreEmpty` 选项(通过 body_ext 透传)
-- **AND** 默认**不改**这两个参数的宜搭侧默认行为
-
-#### Scenario: 删除表单实例
-
-- **WHEN** 调用 `YDClient_Form.deleteForm(conf, formInstanceId, body_ext)`
-- **THEN** 单条删除走 `DELETE` 接口;批量条件删除走 `deleteFormByCondition`
-
-### Requirement: 表单查询原子接口
-
-`YDClient_Form` SHALL 提供分页、ID 列表、全量含子表三种查询粒度,查询方法 MUST 分别对应宜搭官方的不同 endpoint(不混用一个方法承担多种语义)。
-
-#### Scenario: 分页查询
-
-- **WHEN** `searchForm(conf, formUuid, searchFieldJson, currentPage, pageSize, body_ext)`
-- **THEN** 对应 `POST /v1.0/yida/forms/instances/search`
-- **AND** `pageSize` 超过 100 时抛 `McException`(宜搭侧强制上限)
-
-#### Scenario: 全量查询含子表
-
-- **WHEN** `listFormsAll(conf, formUuid, currentPage, pageSize, body_ext)`
-- **THEN** 对应 `retrieve_list_all`(含子表数据),与 `searchForm`(不含子表)行为区分清楚
-
-#### Scenario: 查询组件值
-
-- **WHEN** `listComponentValues(conf, formUuid, fieldId, body_ext)`
-- **THEN** 可获取指定字段的可选项列表
-
-### Requirement: 批量操作
-
-`YDClient_Form` SHALL 提供批量新增与批量 upsert 两个方法,MUST 遵守宜搭侧每批 ≤ 100 条的限制。
-
-#### Scenario: 批量新增
-
-- **WHEN** `batchSaveForm(conf, formUuid, formDataListJson, body_ext)`
-- **THEN** 一次调用批量创建多条记录
-- **AND** 超过 100 条时必须由调用方自行分片(或方法内自动分片,策略见 Service 层)
-
-#### Scenario: 批量 upsert
-
-- **WHEN** `batchUpsertForm(conf, formUuid, searchConditionListJson, dataListJson, body_ext)`
-- **THEN** 对应批量 upsert
-- **AND** `searchCondition` 里日期字段 MUST 用字符串数组格式(规避 `selectListException`)
-
-### Requirement: 操作日志查询
-
-`YDClient_Form` SHALL 提供表单实例的操作历史查询方法。
-
-#### Scenario: 查询实例变更历史
-
-- **WHEN** `listFormOperations(conf, formInstanceId, body_ext)`
-- **THEN** 返回该实例的全部变更历史记录(含操作人 / 操作时间 / 操作类型)
-

+ 0 - 79
openspec/specs/yida-process-atomic/spec.md

@@ -1,79 +0,0 @@
-# yida-process-atomic Specification
-
-## Purpose
-TBD - created by archiving change extend-yida-api-coverage. Update Purpose after archive.
-## Requirements
-### Requirement: 流程实例生命周期
-
-`YDClient_Process` SHALL 覆盖流程发起、终止、撤回、跳转四个关键动作。每个方法 MUST 对应宜搭官方的一个流程实例 endpoint。
-
-#### Scenario: 发起流程
-
-- **WHEN** `startProcess(conf, processCode, formUuid, formDataJson, body_ext)`
-- **THEN** 对应 `POST /v1.0/yida/processes/instances/start`
-- **AND** 返回 `processInstanceId`
-
-#### Scenario: 终止流程
-
-- **WHEN** `terminateProcess(conf, processInstanceId, body_ext)`
-- **AND** `body_ext` 中支持 `operator` / `noExecuteExpression` 等
-
-#### Scenario: 撤回流程
-
-- **WHEN** `revokeProcess(conf, processInstanceId, body_ext)`
-- **THEN** 发起人撤回已提交但未审批完成的流程
-
-### Requirement: 审批任务动作
-
-`YDClient_Process` SHALL 覆盖同意 / 拒绝 / 转交 / 抄送 / 评论五类审批任务动作。每个方法 MUST 与宜搭官方 task 类 endpoint 1:1 对应。
-
-#### Scenario: 同意任务
-
-- **WHEN** `agreeTask(conf, processInstanceId, taskId, comment, body_ext)`
-- **THEN** 对应 `POST /v1.0/yida/processes/tasks/agree`
-
-#### Scenario: 拒绝任务
-
-- **WHEN** `disagreeTask(conf, processInstanceId, taskId, comment, body_ext)`
-- **AND** `body_ext` 支持 `nextOperatorUserIds`(拒绝后指定下一个审批人,部分流程开启此功能)
-
-#### Scenario: 转交任务
-
-- **WHEN** `redirectTask(conf, processInstanceId, taskId, toUserId, comment, body_ext)`
-
-#### Scenario: 抄送
-
-- **WHEN** `ccTask(conf, processInstanceId, taskId, toUserIds, comment, body_ext)`
-
-#### Scenario: 添加评论
-
-- **WHEN** `commentTask(conf, processInstanceId, taskId, comment, body_ext)`
-
-### Requirement: 流程与任务查询
-
-`YDClient_Process` SHALL 提供流程详情、流程列表、任务列表三类查询能力。
-
-#### Scenario: 查单个流程详情
-
-- **WHEN** `getProcess(conf, processInstanceId, body_ext)`
-- **THEN** 返回流程全量信息含已走过的所有节点
-
-#### Scenario: 分页查流程
-
-- **WHEN** `searchProcesses(conf, appType, formUuid, processCode, searchCriteria, body_ext)`
-- **THEN** 支持按发起人 / 状态 / 时间范围过滤
-
-#### Scenario: 查任务列表
-
-- **WHEN** `searchTasks(conf, userId, statuses, body_ext)`
-- **THEN** 返回某用户当前的待办 / 已办任务
-
-### Requirement: 节点跳转
-
-`YDClient_Process` SHALL 提供流程节点跳转能力,允许运维干预流程路径。
-
-#### Scenario: 跳转到指定节点
-
-- **WHEN** `redirectProcess(conf, processInstanceId, targetActivityId, body_ext)`
-- **THEN** 流程跳转到指定节点,支持前跳与后跳
-