Compare commits

..
269 changed files with 3193 additions and 11257 deletions
+133
View File
@@ -0,0 +1,133 @@
```markdown
# certd Development Patterns
> Auto-generated skill from repository analysis
## Overview
This skill teaches the core development patterns and workflows for the `certd` TypeScript codebase. It covers coding conventions, file organization, commit patterns, and detailed step-by-step instructions for extending user preferences—a common feature workflow. The repository is structured for modularity, with clear separation between UI and backend logic, and emphasizes maintainable, convention-driven development.
## Coding Conventions
### File Naming
- **CamelCase** is used for file names.
- Example: `userPreferences.ts`, `preferencesDrawer.vue`
### Import Style
- **Absolute imports** are preferred.
- Example:
```typescript
import { getUserPreferences } from 'packages/ui/certd-client/src/vben/layouts/widgets/preferences/api';
```
### Export Style
- **Named exports** are used throughout the codebase.
- Example:
```typescript
export function getUserPreferences() { ... }
export const PREFERENCE_KEYS = [ ... ];
```
### Commit Patterns
- **Conventional commits** are used, with the `feat` prefix for new features.
- Example:
```
feat: add account sync to preferences
```
## Workflows
### Extend Preferences Feature
**Trigger:** When someone wants to add or enhance a user preference feature (e.g., import/export, sync to account).
**Command:** `/extend-preferences`
Follow these steps to extend user preferences functionality:
1. **Update localization files**
Add or modify strings in:
- `packages/ui/certd-client/src/locales/langs/en-US/preferences.ts`
- `packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts`
```typescript
// en-US/preferences.ts
export default {
sync: "Sync Preferences",
import: "Import Preferences",
// ...
};
```
2. **Modify or add Vue components for preferences UI**
Update or create components such as:
- `preferences-drawer.vue`
- `account-sync.ts`
```vue
<!-- preferences-drawer.vue -->
<template>
<div>
<button @click="syncPreferences">{{ $t('preferences.sync') }}</button>
</div>
</template>
```
3. **Update or add supporting icon definitions**
Edit:
- `packages/ui/certd-client/src/vben/icons/lucide.ts`
```typescript
export const SyncIcon = { /* icon definition */ };
```
4. **Implement or update store logic for settings**
Update:
- `packages/ui/certd-client/src/store/settings/index.tsx`
```typescript
export function syncPreferencesToAccount() { ... }
```
5. **Add or update backend API/controller for user preferences**
Edit or add:
- `packages/ui/certd-client/src/vben/layouts/widgets/preferences/api.ts`
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.ts`
```typescript
// user-preferences.ts
export async function updateUserPreferences(req, res) { ... }
```
6. **Write or update backend tests for new preference logic**
Add or update:
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts`
```typescript
test('should sync preferences', async () => { ... });
```
7. **Update backend models if necessary**
Edit:
- `packages/ui/certd-server/src/modules/mine/service/models.ts`
```typescript
export interface UserPreferences { ... }
```
## Testing Patterns
- **Test files** follow the `*.test.*` naming convention.
- Example: `user-preferences.test.ts`
- **Testing framework** is not explicitly detected, but tests are written in TypeScript and likely use a standard Node.js testing library (e.g., Jest or Mocha).
- **Test Example:**
```typescript
test('should update preferences', async () => {
// Arrange
// Act
// Assert
});
```
## Commands
| Command | Purpose |
|----------------------|--------------------------------------------------------------|
| /extend-preferences | Guide to extend or enhance user preference functionality |
```
+6
View File
@@ -0,0 +1,6 @@
interface:
display_name: "Certd"
short_description: "Repo-specific patterns and workflows for certd"
default_prompt: "Use the certd repo skill to follow existing architecture, testing, and workflow conventions."
policy:
allow_implicit_invocation: true
+227
View File
@@ -0,0 +1,227 @@
{
"version": "1.3",
"schemaVersion": "1.0",
"generatedBy": "ecc-tools",
"generatedAt": "2026-07-30T02:07:44.046Z",
"repo": "https://github.com/certd/certd",
"referenceSetReadiness": {
"score": 0,
"present": 0,
"total": 7,
"items": [
{
"id": "deep-analyzer-corpus",
"label": "Deep analyzer corpus",
"status": "missing",
"evidence": [],
"recommendation": "Add analyzer fixture, golden, benchmark, or reference-set files that can catch analyzer regressions."
},
{
"id": "rag-evaluator",
"label": "RAG/evaluator comparison",
"status": "missing",
"evidence": [],
"recommendation": "Add retrieval or evaluator reference-set comparison fixtures with expected ranking behavior."
},
{
"id": "pr-salvage",
"label": "PR salvage/review corpus",
"status": "missing",
"evidence": [],
"recommendation": "Add stale-PR, review-thread, reopen-flow, or salvage reference cases for queue cleanup automation."
},
{
"id": "discussion-triage",
"label": "Discussion triage corpus",
"status": "missing",
"evidence": [],
"recommendation": "Add public discussion triage fixtures, golden cases, or reference sets for informational, answered, and no-response classifications."
},
{
"id": "harness-compatibility",
"label": "Harness compatibility",
"status": "missing",
"evidence": [],
"recommendation": "Add cross-harness, adapter-compliance, or harness-audit evidence for Claude, Codex, OpenCode, Zed, dmux, and agent surfaces."
},
{
"id": "security-evidence",
"label": "Security evidence",
"status": "missing",
"evidence": [],
"recommendation": "Attach security evidence such as SBOMs, SARIF, audit reports, or AgentShield evidence packs."
},
{
"id": "ci-failure-mode",
"label": "CI failure-mode evidence",
"status": "missing",
"evidence": [],
"recommendation": "Add captured CI failure logs, dry-run fixtures, or troubleshooting docs for common workflow failure modes."
}
]
},
"profiles": {
"requested": "core",
"recommended": "core",
"effective": "core",
"requestedAlias": "core",
"recommendedAlias": "core",
"effectiveAlias": "core"
},
"requestedProfile": "core",
"profile": "core",
"recommendedProfile": "core",
"effectiveProfile": "core",
"tier": "free",
"requestedComponents": [
"repo-baseline"
],
"selectedComponents": [
"repo-baseline"
],
"requestedAddComponents": [],
"requestedRemoveComponents": [],
"blockedRemovalComponents": [],
"tierFilteredComponents": [],
"requestedRootPackages": [
"runtime-core"
],
"selectedRootPackages": [
"runtime-core"
],
"requestedPackages": [
"runtime-core"
],
"requestedAddPackages": [],
"requestedRemovePackages": [],
"selectedPackages": [
"runtime-core"
],
"packages": [
"runtime-core"
],
"blockedRemovalPackages": [],
"tierFilteredRootPackages": [],
"tierFilteredPackages": [],
"conflictingPackages": [],
"dependencyGraph": {
"runtime-core": []
},
"resolutionOrder": [
"runtime-core"
],
"requestedModules": [
"runtime-core"
],
"selectedModules": [
"runtime-core"
],
"modules": [
"runtime-core"
],
"managedFiles": [
".claude/skills/certd/SKILL.md",
".agents/skills/certd/SKILL.md",
".agents/skills/certd/agents/openai.yaml",
".claude/identity.json",
".codex/config.toml",
".codex/AGENTS.md",
".codex/agents/explorer.toml",
".codex/agents/reviewer.toml",
".codex/agents/docs-researcher.toml",
".claude/homunculus/instincts/inherited/certd-instincts.yaml"
],
"packageFiles": {
"runtime-core": [
".claude/skills/certd/SKILL.md",
".agents/skills/certd/SKILL.md",
".agents/skills/certd/agents/openai.yaml",
".claude/identity.json",
".codex/config.toml",
".codex/AGENTS.md",
".codex/agents/explorer.toml",
".codex/agents/reviewer.toml",
".codex/agents/docs-researcher.toml",
".claude/homunculus/instincts/inherited/certd-instincts.yaml"
]
},
"moduleFiles": {
"runtime-core": [
".claude/skills/certd/SKILL.md",
".agents/skills/certd/SKILL.md",
".agents/skills/certd/agents/openai.yaml",
".claude/identity.json",
".codex/config.toml",
".codex/AGENTS.md",
".codex/agents/explorer.toml",
".codex/agents/reviewer.toml",
".codex/agents/docs-researcher.toml",
".claude/homunculus/instincts/inherited/certd-instincts.yaml"
]
},
"files": [
{
"moduleId": "runtime-core",
"path": ".claude/skills/certd/SKILL.md",
"description": "Repository-specific Claude Code skill generated from git history."
},
{
"moduleId": "runtime-core",
"path": ".agents/skills/certd/SKILL.md",
"description": "Codex-facing copy of the generated repository skill."
},
{
"moduleId": "runtime-core",
"path": ".agents/skills/certd/agents/openai.yaml",
"description": "Codex skill metadata so the repo skill appears cleanly in the skill interface."
},
{
"moduleId": "runtime-core",
"path": ".claude/identity.json",
"description": "Suggested identity.json baseline derived from repository conventions."
},
{
"moduleId": "runtime-core",
"path": ".codex/config.toml",
"description": "Repo-local Codex MCP and multi-agent baseline aligned with ECC defaults."
},
{
"moduleId": "runtime-core",
"path": ".codex/AGENTS.md",
"description": "Codex usage guide that points at the generated repo skill and workflow bundle."
},
{
"moduleId": "runtime-core",
"path": ".codex/agents/explorer.toml",
"description": "Read-only explorer role config for Codex multi-agent work."
},
{
"moduleId": "runtime-core",
"path": ".codex/agents/reviewer.toml",
"description": "Read-only reviewer role config focused on correctness and security."
},
{
"moduleId": "runtime-core",
"path": ".codex/agents/docs-researcher.toml",
"description": "Read-only docs researcher role config for API verification."
},
{
"moduleId": "runtime-core",
"path": ".claude/homunculus/instincts/inherited/certd-instincts.yaml",
"description": "Continuous-learning instincts derived from repository patterns."
}
],
"workflows": [],
"adapters": {
"claudeCode": {
"skillPath": ".claude/skills/certd/SKILL.md",
"identityPath": ".claude/identity.json",
"commandPaths": []
},
"codex": {
"configPath": ".codex/config.toml",
"agentsGuidePath": ".codex/AGENTS.md",
"skillPath": ".agents/skills/certd/SKILL.md"
}
}
}
@@ -0,0 +1,486 @@
# Instincts generated from https://github.com/certd/certd
# Generated: 2026-07-30T02:08:05.402Z
# Version: 2.0
# NOTE: This file supplements (does not replace) any existing curated instincts.
# High-confidence manually curated instincts should be preserved alongside these.
---
id: certd-commit-conventional
trigger: "when writing a commit message"
confidence: 0.85
domain: git
source: repo-analysis
source_repo: https://github.com/certd/certd
---
# Certd Commit Conventional
## Action
Use conventional commit format with prefixes: feat
## Evidence
- 2 commits analyzed
- Detected conventional commit pattern
- Examples: feat: 偏好设置支持从剪切板导入, feat: 偏好设置支持保存到账号并在登录后自动同步
---
id: certd-commit-length
trigger: "when writing a commit message"
confidence: 0.6
domain: git
source: repo-analysis
source_repo: https://github.com/certd/certd
---
# Certd Commit Length
## Action
Keep commit messages concise (~22 characters)
## Evidence
- Average commit message length: 22 chars
- Based on 2 commits
---
id: certd-naming-files
trigger: "when creating a new file"
confidence: 0.8
domain: code-style
source: repo-analysis
source_repo: https://github.com/certd/certd
---
# Certd Naming Files
## Action
Use camelCase naming convention
## Evidence
- Analyzed file naming patterns in repository
- Dominant pattern: camelCase
---
id: certd-export-style
trigger: "when exporting from a module"
confidence: 0.7
domain: code-style
source: repo-analysis
source_repo: https://github.com/certd/certd
---
# Certd Export Style
## Action
Prefer named exports
## Evidence
- Export pattern analysis
- Dominant style: named
---
id: certd-arch-feature-based
trigger: "when adding a new feature"
confidence: 0.85
domain: architecture
source: repo-analysis
source_repo: https://github.com/certd/certd
---
# Certd Arch Feature Based
## Action
Create a new folder in src/features/ with all related code colocated
## Evidence
- Feature-based module organization detected
- Structure: src/features/[feature-name]/
---
id: certd-workflow-extend-preferences-feature
trigger: "when doing extend preferences feature"
confidence: 0.6
domain: workflow
source: repo-analysis
source_repo: https://github.com/certd/certd
---
# Certd Workflow Extend Preferences Feature
## Action
Follow the extend-preferences-feature workflow:
1. Update localization files for new preference-related strings
2. Modify or add Vue components for preferences UI
3. Update or add supporting icon definitions
4. Implement or update store logic for settings
5. Add or update backend API/controller for user preferences
6. Write or update backend tests for new preference logic
7. Update backend models if necessary
## Evidence
- Workflow detected from commit patterns
- Frequency: ~2x per month
- Files: packages/ui/certd-client/src/locales/langs/en-US/preferences.ts, packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts, packages/ui/certd-client/src/vben/layouts/widgets/preferences/preferences-drawer.vue
---
id: certd-instinct-file-naming
trigger: "When creating a new file in the codebase"
confidence: 0.9
domain: code-style
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct File Naming
## Action
Name the file using camelCase
## Evidence
- Pattern in codeStyle.namingConventions.files
---
id: certd-instinct-function-naming
trigger: "When defining a new function"
confidence: 0.9
domain: code-style
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Function Naming
## Action
Use camelCase for function names
## Evidence
- Pattern in codeStyle.namingConventions.functions
---
id: certd-instinct-class-naming
trigger: "When defining a new class"
confidence: 0.9
domain: code-style
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Class Naming
## Action
Use PascalCase for class names
## Evidence
- Pattern in codeStyle.namingConventions.classes
---
id: certd-instinct-constant-naming
trigger: "When declaring a constant"
confidence: 0.9
domain: code-style
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Constant Naming
## Action
Use SCREAMING_SNAKE_CASE for constant names
## Evidence
- Pattern in codeStyle.namingConventions.constants
---
id: certd-instinct-import-style
trigger: "When importing modules"
confidence: 0.8
domain: code-style
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Import Style
## Action
Use absolute import paths
## Evidence
- Pattern in codeStyle.importStyle
---
id: certd-instinct-export-style
trigger: "When exporting from a module"
confidence: 0.8
domain: code-style
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Export Style
## Action
Use named exports
## Evidence
- Pattern in codeStyle.exportStyle
---
id: certd-instinct-try-catch
trigger: "When handling errors in code"
confidence: 0.8
domain: code-style
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Try Catch
## Action
Use try-catch blocks for error handling
## Evidence
- Pattern in errorHandling.style
---
id: certd-instinct-test-location
trigger: "When adding or updating tests"
confidence: 0.7
domain: testing
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Test Location
## Action
Place tests in both source and test-specific folders as appropriate (mixed location)
## Evidence
- Pattern in architecture.folderStructure.testLocation
- Seen in files like packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts
---
id: certd-instinct-backend-test-pattern
trigger: "When adding backend logic for user preferences"
confidence: 0.8
domain: testing
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Backend Test Pattern
## Action
Create or update a corresponding .test.ts file in the same directory
## Evidence
- Seen in packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts
---
id: certd-instinct-conventional-commits
trigger: "When writing a commit message"
confidence: 0.9
domain: git
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Conventional Commits
## Action
Use the conventional commit format with a type prefix (e.g., feat: ...)
## Evidence
- Pattern in commits.type
- Examples: feat: 偏好设置支持从剪切板导入
---
id: certd-instinct-commit-length
trigger: "When writing a commit message"
confidence: 0.7
domain: git
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Commit Length
## Action
Keep the commit message concise, around 22 characters on average
## Evidence
- Pattern in commits.averageLength
---
id: certd-instinct-extend-preferences-localization
trigger: "When extending or adding a user preference feature"
confidence: 0.9
domain: workflow
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Extend Preferences Localization
## Action
Update localization files for new preference-related strings
## Evidence
- Workflow: extend-preferences-feature
- Files: packages/ui/certd-client/src/locales/langs/en-US/preferences.ts, packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts
---
id: certd-instinct-extend-preferences-ui
trigger: "When extending or adding a user preference feature"
confidence: 0.9
domain: workflow
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Extend Preferences Ui
## Action
Modify or add Vue components for the preferences UI
## Evidence
- Workflow: extend-preferences-feature
- Files: packages/ui/certd-client/src/vben/layouts/widgets/preferences/preferences-drawer.vue
---
id: certd-instinct-extend-preferences-icons
trigger: "When extending or adding a user preference feature"
confidence: 0.8
domain: workflow
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Extend Preferences Icons
## Action
Update or add supporting icon definitions
## Evidence
- Workflow: extend-preferences-feature
- File: packages/ui/certd-client/src/vben/icons/lucide.ts
---
id: certd-instinct-extend-preferences-store
trigger: "When extending or adding a user preference feature"
confidence: 0.9
domain: workflow
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Extend Preferences Store
## Action
Implement or update store logic for settings
## Evidence
- Workflow: extend-preferences-feature
- File: packages/ui/certd-client/src/store/settings/index.tsx
---
id: certd-instinct-extend-preferences-backend-api
trigger: "When extending or adding a user preference feature"
confidence: 0.9
domain: workflow
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Extend Preferences Backend Api
## Action
Add or update backend API/controller for user preferences
## Evidence
- Workflow: extend-preferences-feature
- Files: packages/ui/certd-server/src/controller/user/mine/user-preferences.ts, packages/ui/certd-server/src/controller/user/mine/user-settings-controller.ts
---
id: certd-instinct-extend-preferences-backend-test
trigger: "When extending or adding a user preference feature"
confidence: 0.9
domain: workflow
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Extend Preferences Backend Test
## Action
Write or update backend tests for new preference logic
## Evidence
- Workflow: extend-preferences-feature
- File: packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts
---
id: certd-instinct-extend-preferences-models
trigger: "When extending or adding a user preference feature and new data is needed"
confidence: 0.8
domain: workflow
source: repo-analysis
source_repo: certd/certd
---
# Certd Instinct Extend Preferences Models
## Action
Update backend models if necessary
## Evidence
- Workflow: extend-preferences-feature
- File: packages/ui/certd-server/src/modules/mine/service/models.ts
+14
View File
@@ -0,0 +1,14 @@
{
"version": "2.0",
"technicalLevel": "technical",
"preferredStyle": {
"verbosity": "detailed",
"codeComments": true,
"explanations": true
},
"domains": [
"typescript"
],
"suggestedBy": "ecc-tools-repo-analysis",
"createdAt": "2026-07-30T02:08:05.402Z"
}
+133
View File
@@ -0,0 +1,133 @@
```markdown
# certd Development Patterns
> Auto-generated skill from repository analysis
## Overview
This skill teaches the core development patterns and workflows for the `certd` TypeScript codebase. It covers coding conventions, file organization, commit patterns, and detailed step-by-step instructions for extending user preferences—a common feature workflow. The repository is structured for modularity, with clear separation between UI and backend logic, and emphasizes maintainable, convention-driven development.
## Coding Conventions
### File Naming
- **CamelCase** is used for file names.
- Example: `userPreferences.ts`, `preferencesDrawer.vue`
### Import Style
- **Absolute imports** are preferred.
- Example:
```typescript
import { getUserPreferences } from 'packages/ui/certd-client/src/vben/layouts/widgets/preferences/api';
```
### Export Style
- **Named exports** are used throughout the codebase.
- Example:
```typescript
export function getUserPreferences() { ... }
export const PREFERENCE_KEYS = [ ... ];
```
### Commit Patterns
- **Conventional commits** are used, with the `feat` prefix for new features.
- Example:
```
feat: add account sync to preferences
```
## Workflows
### Extend Preferences Feature
**Trigger:** When someone wants to add or enhance a user preference feature (e.g., import/export, sync to account).
**Command:** `/extend-preferences`
Follow these steps to extend user preferences functionality:
1. **Update localization files**
Add or modify strings in:
- `packages/ui/certd-client/src/locales/langs/en-US/preferences.ts`
- `packages/ui/certd-client/src/locales/langs/zh-CN/preferences.ts`
```typescript
// en-US/preferences.ts
export default {
sync: "Sync Preferences",
import: "Import Preferences",
// ...
};
```
2. **Modify or add Vue components for preferences UI**
Update or create components such as:
- `preferences-drawer.vue`
- `account-sync.ts`
```vue
<!-- preferences-drawer.vue -->
<template>
<div>
<button @click="syncPreferences">{{ $t('preferences.sync') }}</button>
</div>
</template>
```
3. **Update or add supporting icon definitions**
Edit:
- `packages/ui/certd-client/src/vben/icons/lucide.ts`
```typescript
export const SyncIcon = { /* icon definition */ };
```
4. **Implement or update store logic for settings**
Update:
- `packages/ui/certd-client/src/store/settings/index.tsx`
```typescript
export function syncPreferencesToAccount() { ... }
```
5. **Add or update backend API/controller for user preferences**
Edit or add:
- `packages/ui/certd-client/src/vben/layouts/widgets/preferences/api.ts`
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.ts`
```typescript
// user-preferences.ts
export async function updateUserPreferences(req, res) { ... }
```
6. **Write or update backend tests for new preference logic**
Add or update:
- `packages/ui/certd-server/src/controller/user/mine/user-preferences.test.ts`
```typescript
test('should sync preferences', async () => { ... });
```
7. **Update backend models if necessary**
Edit:
- `packages/ui/certd-server/src/modules/mine/service/models.ts`
```typescript
export interface UserPreferences { ... }
```
## Testing Patterns
- **Test files** follow the `*.test.*` naming convention.
- Example: `user-preferences.test.ts`
- **Testing framework** is not explicitly detected, but tests are written in TypeScript and likely use a standard Node.js testing library (e.g., Jest or Mocha).
- **Test Example:**
```typescript
test('should update preferences', async () => {
// Arrange
// Act
// Assert
});
```
## Commands
| Command | Purpose |
|----------------------|--------------------------------------------------------------|
| /extend-preferences | Guide to extend or enhance user preference functionality |
```
+26
View File
@@ -0,0 +1,26 @@
# ECC for Codex CLI
This supplements the root `AGENTS.md` with a repo-local ECC baseline.
## Repo Skill
- Repo-generated Codex skill: `.agents/skills/certd/SKILL.md`
- Claude-facing companion skill: `.claude/skills/certd/SKILL.md`
- Keep user-specific credentials and private MCPs in `~/.codex/config.toml`, not in this repo.
## MCP Baseline
Treat `.codex/config.toml` as the default ECC-safe baseline for work in this repository.
The generated baseline enables GitHub, Context7, Exa, Memory, Playwright, and Sequential Thinking.
## Multi-Agent Support
- Explorer: read-only evidence gathering
- Reviewer: correctness, security, and regression review
- Docs researcher: API and release-note verification
## Workflow Files
- No dedicated workflow command files were generated for this repo.
Use these workflow files as reusable task scaffolds when the detected repository workflows recur.
+9
View File
@@ -0,0 +1,9 @@
model = "gpt-5.4"
model_reasoning_effort = "medium"
sandbox_mode = "read-only"
developer_instructions = """
Verify APIs, framework behavior, and release-note claims against primary documentation before changes land.
Cite the exact docs or file paths that support each claim.
Do not invent undocumented behavior.
"""
+9
View File
@@ -0,0 +1,9 @@
model = "gpt-5.4"
model_reasoning_effort = "medium"
sandbox_mode = "read-only"
developer_instructions = """
Stay in exploration mode.
Trace the real execution path, cite files and symbols, and avoid proposing fixes unless the parent agent asks for them.
Prefer targeted search and file reads over broad scans.
"""
+9
View File
@@ -0,0 +1,9 @@
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "read-only"
developer_instructions = """
Review like an owner.
Prioritize correctness, security, behavioral regressions, and missing tests.
Lead with concrete findings and avoid style-only feedback unless it hides a real bug.
"""
+48
View File
@@ -0,0 +1,48 @@
#:schema https://developers.openai.com/codex/config-schema.json
# ECC Tools generated Codex baseline
approval_policy = "on-request"
sandbox_mode = "workspace-write"
web_search = "live"
[mcp_servers.github]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
[mcp_servers.context7]
command = "npx"
args = ["-y", "@upstash/context7-mcp@latest"]
[mcp_servers.exa]
url = "https://mcp.exa.ai/mcp"
[mcp_servers.memory]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-memory"]
[mcp_servers.playwright]
command = "npx"
args = ["-y", "@playwright/mcp@latest", "--extension"]
[mcp_servers.sequential-thinking]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]
[features]
multi_agent = true
[agents]
max_threads = 6
max_depth = 1
[agents.explorer]
description = "Read-only codebase explorer for gathering evidence before changes are proposed."
config_file = "agents/explorer.toml"
[agents.reviewer]
description = "PR reviewer focused on correctness, security, and missing tests."
config_file = "agents/reviewer.toml"
[agents.docs_researcher]
description = "Documentation specialist that verifies APIs, framework behavior, and release notes."
config_file = "agents/docs-researcher.toml"
+4 -8
View File
@@ -105,10 +105,8 @@ jobs:
tags: | tags: |
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-slim registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-slim
registry.cn-shenzhen.aliyuncs.com/certd/certd:slim greper/certd:slim
registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}-slim greper/certd:${{steps.get_certd_version.outputs.result}}-slim
certd/certd:slim
certd/certd:${{steps.get_certd_version.outputs.result}}-slim
ghcr.io/${{ github.repository }}:slim ghcr.io/${{ github.repository }}:slim
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-slim ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-slim
@@ -121,10 +119,8 @@ jobs:
tags: | tags: |
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7 registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-armv7 registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-armv7
registry.cn-shenzhen.aliyuncs.com/certd/certd:armv7 greper/certd:armv7
registry.cn-shenzhen.aliyuncs.com/certd/certd:${{steps.get_certd_version.outputs.result}}-armv7 greper/certd:${{steps.get_certd_version.outputs.result}}-armv7
certd/certd:armv7
certd/certd:${{steps.get_certd_version.outputs.result}}-armv7
ghcr.io/${{ github.repository }}:armv7 ghcr.io/${{ github.repository }}:armv7
ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-armv7 ghcr.io/${{ github.repository }}:${{steps.get_certd_version.outputs.result}}-armv7
-88
View File
@@ -1,88 +0,0 @@
name: stable-release
on:
workflow_dispatch:
inputs:
version:
description: "版本号(如 v1.42.5"
required: true
type: string
permissions:
contents: read
packages: write
jobs:
make-stable:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to aliyun container Registry
uses: docker/login-action@v3
with:
registry: registry.cn-shenzhen.aliyuncs.com
username: ${{ secrets.aliyun_cs_username }}
password: ${{ secrets.aliyun_cs_password }}
- name: Login to GitHub Packages
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.dockerhub_username }}
password: ${{ secrets.dockerhub_password }}
# stable 镜像:叠加 ENV 层
- name: Build and push stable images
run: |
echo "FROM greper/certd:${{ inputs.version }}
ENV certd_release_mode=stable" > Dockerfile.stable
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
-f Dockerfile.stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{ inputs.version }}-stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:${{ inputs.version }}-stable \
-t certd/certd:stable \
-t certd/certd:${{ inputs.version }}-stable \
-t ghcr.io/${{ github.repository }}:stable \
-t ghcr.io/${{ github.repository }}:${{ inputs.version }}-stable \
.
# slim-stable 镜像
- name: Build and push slim-stable images
run: |
echo "FROM greper/certd:${{ inputs.version }}-slim
ENV certd_release_mode=stable" > Dockerfile.slim-stable
docker buildx build \
--platform linux/amd64,linux/arm64 \
--push \
-f Dockerfile.slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{ inputs.version }}-slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:slim-stable \
-t registry.cn-shenzhen.aliyuncs.com/certd/certd:${{ inputs.version }}-slim-stable \
-t certd/certd:slim-stable \
-t certd/certd:${{ inputs.version }}-slim-stable \
-t ghcr.io/${{ github.repository }}:slim-stable \
-t ghcr.io/${{ github.repository }}:${{ inputs.version }}-slim-stable \
.
- name: Set AtomGit release as stable
run: |
export ATOMGIT_TOKEN=${{ secrets.ATOMGIT_TOKEN }}
export VERSION=${{ inputs.version }}
npm run set-release-stable
-2
View File
@@ -40,5 +40,3 @@ pnpm-lock.yaml
/popularize/reports/ /popularize/reports/
output/ output/
.uploads/ .uploads/
.certd-plugin-history/
.tmp
@@ -0,0 +1,412 @@
# 插件依赖按需加载方案
## 背景与目标
### 当前问题
- `packages/ui/certd-server/node_modules` 包含 50+ 个插件的所有依赖,体积庞大
- 大量云厂商 SDK(AWS、阿里云、腾讯云、华为云等)只在特定插件中使用
- 用户通常只使用少数几个插件,但必须安装所有依赖
### 目标
实现依赖的按需下载和加载:
1. 插件依赖独立管理,不占用主 `node_modules` 空间
2. 只有当用户首次使用某插件时,才动态下载该插件需要的依赖
3. 依赖安装完成后,通过 `await import()` 从独立路径加载
4. 保持现有插件代码的最小改动
## 当前架构分析
### 插件加载机制
- 插件位于 `packages/ui/certd-server/src/plugins/` 下(50+ 个插件目录)
- `AutoLoadPlugins` 类在启动时扫描 `dist/plugins` 目录并动态导入
- 插件注册到不同的 registry`accessRegistry`, `pluginRegistry`, `dnsProviderRegistry`
- 插件代码已经使用 `await import()` 进行懒加载(如 `await import("@aws-sdk/client-acm")`
### 重型依赖分布
`packages/ui/certd-server/package.json` 分析,以下依赖体积大且仅特定插件使用:
**云厂商 SDK(按插件分组):**
- **AWS 插件**`@aws-sdk/client-acm`, `@aws-sdk/client-cloudfront`, `@aws-sdk/client-iam`, `@aws-sdk/client-route-53`, `@aws-sdk/client-s3`, `@aws-sdk/client-sts`
- **阿里云插件**`@alicloud/openapi-client`, `@alicloud/pop-core`, `@alicloud/tea-typescript`, `@alicloud/fc20230330`
- **腾讯云插件**`tencentcloud-sdk-nodejs`, `cos-nodejs-sdk-v5`
- **华为云插件**`@huaweicloud/huaweicloud-sdk-cdn`, `@huaweicloud/huaweicloud-sdk-core`
- **Azure 插件**`@azure/arm-dns`, `@azure/identity`
- **Google Cloud 插件**`@google-cloud/dns`, `@google-cloud/publicca`
- **火山引擎插件**`@volcengine/openapi`, `@volcengine/tos-sdk`
**网络/工具库:**
- `ssh2`, `socks`, `socks-proxy-agent`SSH 相关插件)
- `ali-oss`, `qiniu`, `basic-ftp`(存储/传输插件)
- `nodemailer`(邮件通知插件)
**通用依赖(保留在主 package.json):**
- `@midwayjs/*` 系列(框架核心)
- `@certd/*` 系列(项目内部包)
- `axios`, `lodash-es`, `dayjs`, `js-yaml` 等基础工具
## 设计方案
### 架构概览
```
packages/ui/certd-server/
├── package.json # 主依赖(框架、通用工具)
├── node_modules/ # 主依赖安装目录
├── optional-deps/ # 新增:可选依赖管理目录
│ ├── package.json # 可选依赖总配置(用于 pnpm install
│ ├── pnpm-lock.yaml # 可选依赖锁文件
│ └── node_modules/ # 可选依赖安装目录
├── src/
│ └── modules/
│ └── dependency/ # 新增:依赖管理模块
│ ├── dependency-manager.ts # 核心:依赖管理器
│ ├── dependency-registry.ts # 依赖注册表(插件 -> 依赖映射)
│ └── types.ts # 类型定义
```
### 核心组件
#### 1. 依赖管理器(DependencyManager
**职责:**
- 检查依赖是否已安装
- 动态执行 `pnpm install` 安装缺失依赖
- 提供从 `optional-deps/node_modules` 加载依赖的方法
- 并发控制:避免多个插件同时触发安装
**关键方法:**
```typescript
class DependencyManager {
// 确保依赖已安装,返回依赖模块
async ensureAndImport<T>(packageName: string): Promise<T>
// 检查依赖是否已安装
async isInstalled(packageName: string): Promise<boolean>
// 安装依赖(带锁,避免并发)
async installDependencies(packages: string[]): Promise<void>
// 从 optional-deps/node_modules 加载依赖
async loadModule<T>(packageName: string): Promise<T>
}
```
**实现要点:**
- 使用文件锁(如 `proper-lockfile`)防止并发安装
- 安装前检查 `optional-deps/node_modules/{packageName}` 是否存在
- 安装命令:`pnpm install --dir optional-deps --ignore-workspace`
- 加载时使用绝对路径:`import('file:///absolute/path/to/optional-deps/node_modules/package')`
#### 2. 依赖注册表(DependencyRegistry
**职责:**
- 维护插件名称到依赖列表的映射
- 提供依赖查询接口
**数据结构:**
```typescript
interface PluginDependencyConfig {
pluginName: string;
dependencies: {
packageName: string;
version: string;
optional?: boolean; // 是否可选(安装失败不阻塞)
}[];
}
// 示例注册
dependencyRegistry.register('plugin-aws', [
{ packageName: '@aws-sdk/client-acm', version: '^3.964.0' },
{ packageName: '@aws-sdk/client-cloudfront', version: '^3.964.0' },
{ packageName: '@aws-sdk/client-route-53', version: '^3.964.0' },
]);
```
#### 3. 插件集成
**改造现有插件代码:**
改造前(`plugin-aws/libs/aws-client.ts`):
```typescript
const { ACMClient, ImportCertificateCommand } = await import("@aws-sdk/client-acm");
```
改造后:
```typescript
import { DependencyManager } from "../../../modules/dependency/dependency-manager.js";
const depManager = new DependencyManager();
const { ACMClient, ImportCertificateCommand } = await depManager.ensureAndImport("@aws-sdk/client-acm");
```
**简化方案(推荐):**
创建辅助函数,减少改动量:
```typescript
// src/modules/dependency/import-helper.ts
export async function importOptionalDep<T>(packageName: string): Promise<T> {
const depManager = new DependencyManager();
return await depManager.ensureAndImport<T>(packageName);
}
// 插件中使用
import { importOptionalDep } from "../../../modules/dependency/import-helper.js";
const { ACMClient } = await importOptionalDep("@aws-sdk/client-acm");
```
### 实施步骤
#### 阶段一:基础设施搭建
1. 创建 `optional-deps/` 目录结构
2. 生成 `optional-deps/package.json`(包含所有可选依赖)
3. 实现 `DependencyManager` 核心逻辑
4. 实现依赖安装锁机制
5. 编写单元测试
#### 阶段二:依赖迁移
6. 从主 `package.json` 移除可选依赖
7. 将依赖添加到 `optional-deps/package.json`
8. 创建依赖注册表,映射插件到依赖
#### 阶段三:插件改造
9. 创建 `import-helper.ts` 辅助函数
10. 逐步改造插件代码,使用 `importOptionalDep` 加载依赖
11. 优先改造重型依赖(AWS、阿里云、腾讯云等)
#### 阶段四:测试与优化
12. 端到端测试:验证依赖按需安装和加载
13. 性能优化:缓存已加载的模块
14. 错误处理:安装失败时的降级策略
15. 文档:编写使用说明和迁移指南
## 关键技术决策
### 1. 依赖分组策略
**选择:按插件分组**
- 每个插件声明自己需要的依赖
- 优点:职责清晰,易于维护
- 缺点:可能有重复依赖(但 pnpm 会去重)
**备选:按功能分组**
- 将依赖按功能分组(如 "aws-deps", "aliyun-deps"
- 优点:更细粒度控制
- 缺点:增加复杂度
### 2. 安装触发时机
**选择:首次使用时触发**
- 在插件的 `execute()``getClient()` 方法中触发安装
- 优点:真正的按需加载
- 缺点:首次使用有延迟
**备选:启动时预检查**
- 启动时扫描启用的插件,预安装依赖
- 优点:避免运行时延迟
- 缺点:可能安装不需要的依赖
### 3. 依赖路径解析
**选择:使用绝对路径 + `file://` 协议**
```typescript
const modulePath = path.resolve(__dirname, '../../optional-deps/node_modules', packageName);
return await import(`file://${modulePath}/index.js`);
```
**原因:**
- Node.js ESM 要求明确的 URL 格式
- 避免模块解析冲突
### 4. 并发控制
**选择:文件锁 + 内存锁双重保护**
- 使用 `proper-lockfile` 锁定 `optional-deps/` 目录
- 内存中使用 `Map` 记录正在安装的依赖
- 避免多个插件同时触发安装
### 5. 错误处理
**策略:**
- 安装失败时记录日志,抛出明确的错误信息
- 提供手动安装命令提示:`请运行: cd optional-deps && pnpm install`
- 支持降级:某些非核心依赖安装失败时,插件可以部分功能可用
## 验证方案
### 单元测试
1. 测试 `DependencyManager.isInstalled()` 正确检测依赖状态
2. 测试 `DependencyManager.installDependencies()` 成功安装依赖
3. 测试并发安装时的锁机制
4. 测试从 `optional-deps/node_modules` 加载模块
### 集成测试
1. 清空 `optional-deps/node_modules`
2. 启动服务,验证不触发安装
3. 调用 AWS 插件,验证触发安装并成功加载
4. 再次调用,验证不重复安装
5. 验证主 `node_modules` 体积减少
### 性能测试
1. 测量首次安装依赖的耗时
2. 测量后续加载的耗时(应该与正常 import 相近)
3. 对比改造前后的 `node_modules` 大小
## 风险与挑战
### 1. 首次使用延迟
**风险:** 用户首次使用插件时需要等待依赖安装(可能几十秒)
**缓解:**
- 在 UI 上显示安装进度
- 提供预安装命令:`pnpm run install-optional-deps`
- 文档说明首次使用会有延迟
### 2. 离线环境
**风险:** 离线环境无法下载依赖
**缓解:**
- 提供完整安装包(包含所有可选依赖)
- 支持手动复制 `node_modules`
### 3. 版本冲突
**风险:** 可选依赖与主依赖版本冲突
**缓解:**
- 使用 `--ignore-workspace` 隔离安装
- 定期同步主依赖版本
### 4. TypeScript 类型
**风险:** 动态导入的类型推断
**缓解:**
- 保留 `@types/*` 在主 `devDependencies`
- 使用泛型和类型断言
## 预期收益
1. **空间节省:**`node_modules` 体积减少 60-70%(估算)
2. **安装速度:** 初始 `pnpm install` 速度提升 3-5 倍
3. **用户体验:** 不使用的插件不占用空间,按需加载
4. **维护性:** 依赖分组清晰,易于管理
## 后续优化
1. **依赖预热:** 在后台预安装常用插件依赖
2. **依赖缓存:** 支持从 CDN 或本地缓存安装
3. **依赖更新:** 提供命令批量更新可选依赖
4. **插件市场:** 支持从远程下载插件及其依赖配置
## 附录:依赖分类清单
### 可选依赖(迁移到 optional-deps/package.json
**AWS 相关(plugin-aws, plugin-aws-cn):**
```json
{
"@aws-sdk/client-acm": "^3.964.0",
"@aws-sdk/client-cloudfront": "^3.964.0",
"@aws-sdk/client-iam": "^3.964.0",
"@aws-sdk/client-route-53": "^3.964.0",
"@aws-sdk/client-s3": "^3.964.0",
"@aws-sdk/client-sts": "^3.990.0"
}
```
**阿里云相关(plugin-aliyun, plugin-lib/aliyun):**
```json
{
"@alicloud/fc20230330": "^4.1.7",
"@alicloud/openapi-client": "^0.4.12",
"@alicloud/openapi-util": "^0.3.2",
"@alicloud/pop-core": "^1.7.10",
"@alicloud/sts-sdk": "^1.0.2",
"@alicloud/tea-typescript": "^1.8.0",
"@alicloud/tea-util": "^1.4.10",
"ali-oss": "^6.21.0"
}
```
**腾讯云相关(plugin-tencent, plugin-lib/tencent):**
```json
{
"tencentcloud-sdk-nodejs": "^4.1.112",
"cos-nodejs-sdk-v5": "^2.14.6"
}
```
**华为云相关(plugin-huawei):**
```json
{
"@huaweicloud/huaweicloud-sdk-cdn": "3.1.185",
"@huaweicloud/huaweicloud-sdk-core": "3.1.185",
"@huaweicloud/huaweicloud-sdk-elb": "3.1.185",
"@huaweicloud/huaweicloud-sdk-iam": "3.1.185",
"esdk-obs-nodejs": "^3.25.6"
}
```
**Azure 相关(plugin-azure):**
```json
{
"@azure/arm-dns": "^5.1.0",
"@azure/identity": "^4.13.1"
}
```
**Google Cloud 相关(plugin-google, plugin-cert/google):**
```json
{
"@google-cloud/dns": "^5.3.1",
"@google-cloud/publicca": "^1.3.0"
}
```
**火山引擎相关(plugin-volcengine):**
```json
{
"@volcengine/openapi": "^1.28.1",
"@volcengine/tos-sdk": "^2.9.1"
}
```
**SSH/网络相关(plugin-host, plugin-lib/ssh):**
```json
{
"ssh2": "^1.17.0",
"socks": "^2.8.3",
"socks-proxy-agent": "^8.0.4",
"basic-ftp": "^5.0.5"
}
```
**其他存储/传输(plugin-qiniu, plugin-lib/qiniu):**
```json
{
"qiniu": "^7.12.0"
}
```
**邮件通知(plugin-notification/email):**
```json
{
"nodemailer": "^6.9.16"
}
```
### 主依赖(保留在主 package.json
**框架核心:**
- `@midwayjs/*` 系列
- `@koa/cors`
- `typeorm`, `better-sqlite3`, `mysql2`, `pg`
**项目内部包:**
- `@certd/*` 系列
**通用工具:**
- `axios`, `lodash-es`, `dayjs`, `js-yaml`
- `crypto-js`, `jsonwebtoken`, `bcryptjs`
- `reflect-metadata`, `uuid`, `nanoid`
- 等等
## 总结
本方案通过引入独立的可选依赖管理机制,实现了插件依赖的按需下载和加载。核心思路是:
1. **隔离管理:**`optional-deps/` 目录下维护独立的 `package.json``node_modules`
2. **动态安装:** 通过 `DependencyManager` 在首次使用时触发 `pnpm install`
3. **路径加载:** 使用绝对路径从独立目录加载依赖模块
4. **最小改动:** 通过辅助函数 `importOptionalDep` 简化插件代码改造
该方案可以显著减少主 `node_modules` 体积,提升初始安装速度,同时保持现有架构的兼容性和可维护性。
@@ -1,164 +0,0 @@
---
name: certd-online-plugin-dev
description: 用于通过 Certd API 开发、修改、读取、保存和恢复在线插件。当用户要求使用 Codex 或 Trae 创建 Task、DNS Provider、Access 插件,或优化已有 Certd 插件时使用。
---
# Certd 在线插件开发 Skill
## 开发方式
本 Skill 使用 Certd HTTP API,不使用 WebSocket,也不依赖浏览器传递代码草稿。
- 前端只生成包含需求、API 地址和认证 Token 的启动提示词。
- Agent 直接调用 Certd API 读取和保存插件 YAML。
- Agent 的进度、日志和代码修改在 Codex/Trae 中查看。
- 插件开发临时文件和修改历史统一保存在 Agent 工作区的 `.tmp/online-plugin-dev/` 下,不保存到浏览器或 Certd 后端。
## 启动检查
开始开发前先确认当前工作目录是否已经在 Certd 项目内:
- 应存在 `package.json`
- 应存在 `packages/ui/certd-server/src/plugins/`
- 应存在 `.trae/skills/`
- 应存在 `.trae/skills/certd-online-plugin-dev/SKILL.md`
如果当前目录不是 Certd 项目,或缺少 `certd-online-plugin-dev` Skill,先拉取 Certd 仓库代码并切换到仓库内工作( --depth 1 拉取第一层即可)
1. 优先使用 `https://atomgit.com/certd/certd/`
2. 如果 AtomGit 拉取失败,再使用 `https://github.com/certd/certd`
3. 拉取后重新检查上述特征,并读取 `.trae/skills/certd-online-plugin-dev/SKILL.md`
开发插件时,参考 Certd 项目下已有内置插件实现:`packages/ui/certd-server/src/plugins/`
## 插件来源
Certd 插件按来源分为三类:
- 内置插件:`type: "builtIn"`,随 Certd 安装包提供。可读取并在流水线中使用,不应通过本 Skill 修改或覆盖。
- 市场插件:`type: "store"`,且存在 `appId``developerId`。它来自在线插件市场,可能尚未安装到本地;是否可修改只能以接口返回的 `editable` 为准。
- 本地插件:`type: "store"`,但没有 `appId``developerId`。它是当前 Certd 实例本地创建、导入或复制的插件,可直接保存;发布到市场后会带上市场归属信息。
不要只根据 `type: "store"` 判断插件是否来自市场,也不要自行推断编辑权限;始终使用列表结果中的 `editable` 字段。
## API 认证
提示词会提供 Certd API 地址和仅限 AI 插件开发接口的受限 Token。调用 API 时使用:
```http
Authorization: <token>
Content-Type: application/json
```
不要把 Token 写入代码、历史摘要、日志、提交信息或插件 YAML。
所有 Certd API 请求统一使用 Node.js 18+ 的 `fetch`。不要使用 PowerShell 的 `Invoke-RestMethod``Invoke-WebRequest` 或 .NET HTTP 客户端发送插件 YAML/JSON;它们在 Windows 上可能造成中文乱码或使完整 YAML 导入请求长时间无响应。
Node 请求须直接读取 UTF-8 文件或在 Node 内构造 JSON,并使用 `JSON.stringify`
```javascript
const response = await fetch(`${apiBase}/scoped/sys/ai/plugin/find`, {
method: "POST",
headers: { Authorization: token, "Content-Type": "application/json; charset=utf-8" },
body: JSON.stringify({ keywords: ["nginx"], includeBuiltIn: true, includeStore: true }),
})
```
## UTF-8 保存
在 Windows 上,Node 直接以 UTF-8 读取 YAML 并用 `JSON.stringify` 发送;不要让 PowerShell 转发含中文的 YAML/JSON。保存后检查中文字段不含 `?`,再调用 `/scoped/sys/ai/plugin/find``/scoped/sys/ai/plugin/info` 验证。
## API 工作流
1. 使用 `/scoped/sys/ai/plugin/find` 查询插件和 Access,可通过 `keywords` 数组传递多个关键词。
2. 查询结果包含 `editable`
- `editable: true`:允许当前 Agent 修改并保存。
- `editable: false`:只能读取和使用,不能修改。
3. 读取完整 YAML 时调用 `/scoped/sys/ai/plugin/export`
4. 所有插件保存统一调用 `/scoped/sys/ai/plugin/import`,并始终传递完整 YAML
- 新插件使用 `override: false`
- 已有插件使用 `override: true`;导入接口根据 `author``name` 定位并覆盖已有记录。
5. 不使用 `/sys/plugin/add``/sys/plugin/update` 保存插件,避免保存路径分叉、字段丢失和 Windows 请求兼容性问题。
6. 保存完成后重新调用 `/scoped/sys/ai/plugin/find``/scoped/sys/ai/plugin/info` 验证结果。
`/scoped/sys/ai/plugin/find` 会在一次请求中分别查询内置插件和 `store` 插件,再合并返回;`store` 插件需按上述字段区分市场插件与本地插件。
详细请求字段见 `references/certd-api.md`
## Access 协作
开发 Task 或 DNS Provider 前,先用 `/scoped/sys/ai/plugin/find` 查询对应 Access
1. 如果没有对应 Access,先创建 Access 插件,再创建业务插件。
2. 如果已有 Access,先读取它的完整 YAML 和 `content`
3. 如果 Access 已提供所需 API/SDK,业务插件优先复用。
4. 如果缺少能力:
- `editable: true`:优先修改 Access,并先保存历史。
- `editable: false`:在当前业务插件中实现必要的 API 调用。
5. 业务插件通过 `dependPlugins` 声明 Access 依赖。
详细规则见 `references/access-development.md`
## 本地历史
开发插件时,必须在当前工作区创建并使用 `.tmp/online-plugin-dev/` 作为临时目录。历史记录、临时 YAML、脚本草稿和调试记录都放在该目录下。
每次修改插件前,必须将完整 YAML 保存到 `.tmp/online-plugin-dev/history/`
```text
.tmp/online-plugin-dev/
history/
plugin-12/
2026-08-02T12-30-00-before-edit.yaml
2026-08-02T12-30-00-change.md
```
保存要求:
- 修改前保存完整 YAML。
- 修改后保存修改摘要。
- 恢复前再次备份当前版本。
- 不上传历史文件,不保存 Token、证书、私钥或真实授权值。
详细格式见 `references/local-history.md`
## YAML 和脚本规范
插件始终以完整 YAML 传递和保存,脚本源码放在顶层 `content` 字段。
- 统一使用 `await _ctx.import(...)` 引用模块。
- `"/@/..."` 表示以绝对路径引用 `server/src/` 下的模块。
- 最后返回继承目标基类的 class。
- 不使用 `import``export`、装饰器或独立源码文件语法。
- 使用 `this.logger` 打印插件执行日志。
- 使用 `this.ctx.http` 访问 HTTP 能力。
- 失败时抛出 `Error`
需要字段格式时读取 `references/online-yaml-format.md`
需要组件示例时读取 `references/component-examples.md`
## 示例插件
开发对应类型插件前,先读取 `examples/` 下的示例:
- Access`examples/DemoAccess.yaml`
- 部署/Task`examples/DemoDeploy.yaml`
- DNS Provider`examples/DemoDnsProvider.yaml`
示例是完整在线插件 YAML,重点参考 `input` 配置、依赖声明和 `content` 脚本结构。
## 子 Skill
- Task`skills/task-plugin-dev/SKILL.md`
- DNS Provider`skills/dns-provider-dev/SKILL.md`
- Access`skills/access-plugin-dev/SKILL.md`
## 安全边界
Certd 会保存证书、私钥、API Token、云厂商密钥、SSH 凭据和其他敏感授权。
- 禁止读取、打印或上传真实授权值。
- 禁止把认证 Token 写入插件、历史、日志或摘要。
- 禁止读取无关的证书、私钥、Cookie、环境变量和系统设置。
- 只使用脱敏示例数据和公开文档。
- 不要自动发布;保存、测试、审核和发布由用户确认。
@@ -1,4 +0,0 @@
interface:
display_name: "Certd 在线插件开发"
short_description: "通过 Certd API 使用 Codex 或 Trae 开发在线插件"
default_prompt: "读取在线插件 YAML 和对应类型规范,按需求修改 content,保存历史记录后通过 Certd API 写回。"
@@ -1,37 +0,0 @@
name: DemoAccess
icon: logos:airflow-icon
title: Demo-授权插件示例 # 模块-插件名
group: null
desc: 这只是一个示例
version: 1.0.0
pluginType: access
author: greper
input:
username:
title: 用户名
required: true
encrypt: false
component:
name: a-input
allowClear: true
password:
title: 密码
required: true
encrypt: true
component:
name: a-input
allowClear: true
showRunStrategy: false
default:
strategy:
runStrategy: 1
content: |
// 必须使用 await import 来引入模块
const { BaseAccess } = await import("@certd/pipeline")
// 需要返回一个继承BaseAccess的类
return class DemoAccess extends BaseAccess {
// 授权的字段,跟左边input一一对应
username;
password;
}
@@ -1,100 +0,0 @@
name: DemoDeploy
icon: logos:amp-icon
title: Demo-部署插件示例 # 模块-插件名
group: cdn
desc: 这仅仅是一个示例
version: 1.0.0
pluginType: deploy
author: greper
input:
cert:
title: 前置任务证书
helper: 请选择前置任务产生的证书
component:
name: output-selector
vModel: modelValue
from:
- ':cert:'
required: true
certDomains:
title: 当前证书域名
component:
name: cert-domains-getter
mergeScript: |
return {
component:{
inputKey: ctx.compute(({form})=>{
return form.cert
}),
}
}
required: true
accessId:
title: Access授权
helper: xxxx的授权
component:
name: access-selector
type: aliyun
required: true
key1:
title: 输入示例1
required: false
key2:
title: 可选项
component:
name: a-select
vMode: value
options:
- value: '1'
label: 选项1
- value: '2'
label: 选项2
required: false
showRunStrategy: false
default:
strategy:
runStrategy: 1
content: >
// 要用await来import模块
const { AbstractTaskPlugin } = await _ctx.import("@certd/pipeline")
// 使用_ctx.import("/@/xxx.js") 以绝对路径引用模块,/@相当于根路径
const {AliyunAccess} = await _ctx.import("/@/plugins/plugin-lib/aliyun/access/index.js")
_ctx.logger.info("AliyunAccess:",AliyunAccess)
// 要返回一个继承AbstractTaskPlugin的class
return class DemoTask extends AbstractTaskPlugin {
// 这里是插件的输入参数,对应左边的input配置
cert;
certDomains;
accessId;
key1;
key2;
// 编写执行方法
async execute(){
// 根据accessId获取授权配置
const access = await this.getAccess(this.accessId)
//必须使用this.logger打印日志
// this.logger.info("cert:",this.cert);
this.logger.info("certDomains:",this.certDomains);
this.logger.info("access:",access);
this.logger.info("key1:",this.key1);
this.logger.info("key2:",this.key2);
this.logger.info("开始xxx部署任务")
// 你的部署任务代码 【必须实现】
// this.ctx里面有一些常用的方法类,比如utils、http、logger等
const res = await this.ctx.http.request({url:"https://www.baidu.com"})
if(res.error){
//抛出异常,终止任务,否则将被判定为执行成功
throw new Error("部署失败:"+res.message)
}
this.logger.info("执行成功")
// this.outputName = xxxx //设置输出参数,可以被其他插件选择使用
}
}
@@ -1,119 +0,0 @@
name: DemoDnsProvider
icon: fa-solid:frog
title: Demo-Dns提供商插件示例 # 模块-插件名
desc: 这只是一个示例
type: custom
version: 1.0.0
pluginType: dnsProvider
author: greper
accessType: aliyun # 需要的授权类型
showRunStrategy: false
default:
strategy:
runStrategy: 1
content: |+
const { AbstractDnsProvider } = await _ctx.import("@certd/pipeline")
return class DemoDnsProvider extends AbstractDnsProvider {
// 创建dns解析记录,用于验证域名所有权 【必须实现】
async createRecord(options) {
/**
* fullRecord: '_acme-challenge.test.example.com',
* value: 一串uuid
* type: 'TXT',
* domain: 'example.com'
*/
const { fullRecord, value, type, domain } = options;
const access = this.ctx.access
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
// const record = await sdk.createRecord() // 调用对应的接口创建解析记录
//返回解析记录,用于后面清理
return record
}
/**
* 删除dns解析记录,清理申请痕迹【必须实现】
* @param options
*/
async removeRecord(options) {
const { fullRecord, value } = options.recordReq;
const record = options.recordRes; // createRecord接口返回的record
const access = this.ctx.access
this.logger.info('删除域名解析:', fullRecord, value);
if (!record) {
this.logger.info('record为空,不执行删除');
return;
}
const recordId = record.id;
// 这里调用删除txt dns解析记录接口
// sdk.removeRecord(recordId)
this.logger.info("删除域名解析成功");
}
/**
* 获取域名列表 【可选,没有实现的话,不支持在certd域名管理中导入域名列表,不影响证书申请】
* @param req
* @returns
*/
async getDomainListPage(req: PageSearch): Promise<PageRes<DomainRecord>> {
const pager = new Pager(req);
const params = {
RegionId: "cn-hangzhou",
PageSize: pager.pageSize,
PageNumber: pager.pageNo,
};
const requestOption = {
method: "POST",
};
const ret = await this.client.request("DescribeDomains", params, requestOption);
const list =
ret.Domains?.Domain?.map(item => ({
id: item.DomainId,
domain: item.DomainName,
})) || [];
return {
list,
total: ret.TotalCount,
};
}
/**
* 获取域名解析记录列表 【可选,没有实现的话,不支持在站点监控里面导入网址,不影响证书申请】
* @param domain
* @param req
* @returns
*/
async getRecordListPage(domain: string, req: PageSearch): Promise<PageRes<DnsResolveRecord>> {
const pager = new Pager(req);
const params = {
RegionId: "cn-hangzhou",
DomainName: domain,
PageSize: pager.pageSize,
PageNumber: pager.pageNo,
};
const requestOption = {
method: "POST",
};
const ret = await this.client.request("DescribeDomainRecords", params, requestOption);
const rawList = ret.DomainRecords?.Record || [];
const list = rawList.map(item => ({
id: item.RecordId,
hostRecord: item.RR,
fullRecord: item.RR === "@" ? domain : `${item.RR}.${domain}`,
type: item.Type,
value: item.Value,
}));
return {
list,
total: ret.TotalCount,
};
}
}
@@ -1,19 +0,0 @@
# Access 开发规范
Access 插件负责保存授权配置,也负责封装平台 API/SDK,供 Task 和 DNS Provider 复用。
## 查询顺序
1. 调用 `/scoped/sys/ai/plugin/find`,使用 `pluginType: access`
2. 根据 `name``author``fullName` 识别目标 Access。
3. 使用 `/scoped/sys/ai/plugin/export` 读取完整 YAML。
4. 检查 `content` 中已经提供的方法。
## 修改规则
- Access 的 `editable: true` 时才允许修改。
- 修改前先保存本地历史。
- 优先把通用 API/SDK 能力放入 Access。
- 业务插件通过 `dependPlugins` 依赖 Access。
- `editable: false` 时不要尝试修改 Access,在业务插件内部实现必要的调用。
- 不要在日志中打印完整授权配置。
@@ -1,70 +0,0 @@
# Certd API
以下接口都以前端生成提示词中的 API 地址为基础地址,并使用 `Authorization` 请求头。
## 查询插件
```http
POST /scoped/sys/ai/plugin/find
Content-Type: application/json
Authorization: <token>
```
请求示例:
```json
{
"keywords": ["aliyun", "dns"],
"pluginType": "access",
"includeBuiltIn": true,
"includeStore": true
}
```
接口会分别查询内置插件和 `store` 插件,再合并返回:
- `type: "builtIn"`:内置插件,不通过在线开发 API 修改。
- `type: "store"` 且有 `appId``developerId`:市场插件。
- `type: "store"` 且没有 `appId``developerId`:本地插件。
结果中的 `editable` 是唯一的编辑权限依据;不能只按插件来源判断是否可修改。
列表结果只返回插件基础信息,不返回 `content``setting``sysSetting``metadata``extra`。需要完整 YAML 时再调用 `/scoped/sys/ai/plugin/export`
## 读取插件信息
```http
POST /scoped/sys/ai/plugin/info?id=12
Authorization: <token>
```
## 导出完整 YAML
```http
POST /scoped/sys/ai/plugin/export
Content-Type: application/json
Authorization: <token>
```
```json
{
"id": 12
}
```
## 保存插件
使用完整 YAML 导入:
```http
POST /scoped/sys/ai/plugin/import
```
```json
{
"content": "完整 YAML",
"override": true,
"type": "store"
}
```
保存后重新调用 `/scoped/sys/ai/plugin/find``/scoped/sys/ai/plugin/info` 验证。
@@ -1,423 +0,0 @@
# Component Examples
Source: `packages/ui/certd-server/src/plugins`.
Detected **30** distinct component names.
These snippets are extracted from existing Certd plugins. For online plugins, place the object under `input.<field>.component` in the YAML document.
## `EmailSelector`
Source: `packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-deploy-to-mail.ts`
```ts
component: {
name: "EmailSelector",
vModel: "value",
mode: "tags",
}
```
## `ParamsShow`
Source: `packages/ui/certd-server/src/plugins/plugin-template/email/plugin-common.ts`
```ts
component: {
name: "ParamsShow",
params: [
{ label: "标题", value: "title" },
{ label: "内容", value: "content" },
{ label: "URL", value: "url" },
],
}
```
## `RemoteSelect`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/getter/aliyun.ts`
```ts
component: {
name: "RemoteSelect",
vModel: "value",
pager: true,
single: true,
}
```
## `a-alert`
Source: `packages/ui/certd-server/src/plugins/plugin-template/email/plugin-base.ts`
```ts
component: {
name: "a-alert",
props: {
type: "info",
message: "在标题和内容模版中,通过${name}引用参数,例如: 感谢注册,您的注册验证码为:${code}",
},
}
```
## `a-auto-complete`
Source: `packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-ack/index.ts`
```ts
component: {
name: "a-auto-complete",
vModel: "value",
options: [
{ value: "cn-qingdao", label: "华北1(青岛)" },
{ value: "cn-beijing", label: "华北2(北京)" },
{ value: "cn-zhangjiakou", label: "华北3(张家口)" },
{ value: "cn-huhehaote", label: "华北5(呼和浩特)" },
{ value: "cn-wulanchabu", label: "华北6(乌兰察布)" },
{ value: "cn-hangzhou", label: "华东1(杭州)" },
{ value: "cn-shanghai", label: "华东2(上海)" },
{ value: "cn-shenzhen", label: "华南1(深圳)" },
{ value: "cn-guangzhou", label: "华南3(广州)" },
{ value: "ap-southeast-2", label: "澳大利亚(悉尼)" },
{ value: "ap-southeast-3", label: "马来西亚(吉隆坡)" },
{ value: "ap-northeast-1", label: "日本(东京)" },
{ value: "cn-chengdu", label: "西南1(成都)" },
{ value: "ap-southeast-1", label: "新加坡" },
{ value: "ap-southeast-5", label: "印度尼西亚(雅加达)" },
{ value: "cn-hongkong", label: "中国香港" },
{ value: "eu-central-1", label: "德国(法兰克福)" },
{ value: "us-east-1", label: "美国(弗吉尼亚)" },
{ value: "us-west-1", label: "美国(硅谷)" },
{ value: "eu-west-1", label: "英国(伦敦)" },
{ value: "me-east-1", label: "阿联酋(迪拜)" },
//金融云
{ value: "cn-beijing-finance-1", label: "华北2 金融云(邀测)" },
{ value: "cn-hangzhou-finance", label: "华东1 金融云" },
{ value: "cn-shanghai-finance-1", label: "华东2 金融云" },
{ value: "cn-shenzhen-finance-1", label: "华南1 金融云" },
],
placeholder: "集群所属大区",
}
```
## `a-input`
Source: `packages/ui/certd-server/src/plugins/plugin-admin/plugin-db-backup.ts`
```ts
component: {
name: "a-input",
type: "value",
placeholder: `默认${defaultBackupDir}`,
}
```
## `a-input-number`
Source: `packages/ui/certd-server/src/plugins/plugin-acepanel/access.ts`
```ts
component: {
name: "a-input-number",
vModel: "value",
}
```
## `a-input-password`
Source: `packages/ui/certd-server/src/plugins/plugin-51dns/access.ts`
```ts
component: {
name: "a-input-password",
vModel: "value",
placeholder: "密码",
}
```
## `a-radio-group`
Source: `packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-esa/index.ts`
```ts
component: {
name: "a-radio-group",
vModel: "value",
options: [
{ label: "边缘证书", value: "edge" },
{ label: "SaaS证书", value: "saas" },
],
}
```
## `a-select`
Source: `packages/ui/certd-server/src/plugins/plugin-admin/plugin-db-backup.ts`
```ts
component: {
name: "a-select",
options: [
{ label: "本地复制", value: "local" },
{ label: "oss上传(推荐)", value: "oss" },
{ label: "ssh上传(请使用oss上传方式)", value: "ssh", disabled: true },
],
placeholder: "",
}
```
## `a-switch`
Source: `packages/ui/certd-server/src/plugins/plugin-acepanel/access.ts`
```ts
component: {
name: "a-switch",
vModel: "checked",
}
```
## `a-textarea`
Source: `packages/ui/certd-server/src/plugins/plugin-admin/plugin-script.ts`
```ts
component: {
name: "a-textarea",
vModel: "value",
rows: 10,
style: "background-color: #000c17;color: #fafafa;",
}
```
## `access-selector`
Source: `packages/ui/certd-server/src/plugins/plugin-acepanel/plugins/plugin-deploy-to-website.ts`
```ts
component: {
name: "access-selector",
type: "acepanel",
}
```
## `api-test`
Source: `packages/ui/certd-server/src/plugins/plugin-51dns/access.ts`
```ts
component: {
name: "api-test",
action: "TestRequest",
}
```
## `cert-info-updater`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/custom/index.ts`
```ts
component: {
name: "cert-info-updater",
vModel: "modelValue",
}
```
## `dns-provider-selector`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.ts`
```ts
component: {
name: "dns-provider-selector",
}
```
## `domain-selector`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base-convert.ts`
```ts
component: {
name: "domain-selector",
vModel: "value",
mode: "tags",
// open: false,
placeholder: "请输入证书域名/IP,比如:foo.com , *.foo.com , *.sub.foo.com , *.bar.com , 123.123.123.123",
tokenSeparators: [",", " ", "", "、", "|"],
search: true,
pager: true,
}
```
## `domains-verify-plan-editor`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.ts`
```ts
component: {
name: "domains-verify-plan-editor",
}
```
## `email-selector`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base.ts`
```ts
component: {
name: "email-selector",
vModel: "value",
}
```
## `fs-icon-selector`
Source: `packages/ui/certd-server/src/plugins/plugin-oauth/oidc/plugin-oidc.ts`
```ts
component: {
name: "fs-icon-selector",
vModel: "modelValue",
iconSets: IconSets,
}
```
## `icon-select`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.ts`
```ts
component: {
name: "icon-select",
vModel: "value",
options: [
{ value: "letsencrypt", label: "Let's Encrypt(免费,新手推荐,支持IP证书)", icon: "simple-icons:letsencrypt" },
{ value: "google", label: "Google(免费)", icon: "flat-color-icons:google" },
{ value: "zerossl", label: "ZeroSSL(免费)", icon: "emojione:digit-zero" },
{ value: "litessl", label: "litessl(免费)", icon: "roentgen:free" },
{ value: "sslcom", label: "SSL.com(仅主域名和www免费)", icon: "la:expeditedssl" },
{ value: "letsencrypt_staging", label: "Let's Encrypt测试环境(仅供测试)", icon: "simple-icons:letsencrypt" },
],
}
```
## `input-password`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/base-convert.ts`
```ts
component: {
name: "input-password",
vModel: "value",
}
```
## `notification-selector`
Source: `packages/ui/certd-server/src/plugins/plugin-github/plugins/plugin-check-release.ts`
```ts
component: {
name: "notification-selector",
select: {
mode: "tags",
},
}
```
## `output-selector`
Source: `packages/ui/certd-server/src/plugins/plugin-acepanel/plugins/plugin-deploy-to-website.ts`
```ts
component: {
name: "output-selector",
from: [...CertApplyPluginNames],
}
```
## `pem-input`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/custom/index.ts`
```ts
component: {
name: "pem-input",
vModel: "modelValue",
textarea: {
rows: 4,
placeholder: "-----BEGIN CERTIFICATE-----\n...\n...\n-----END CERTIFICATE-----",
},
}
```
## `refresh-input`
Source: `packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.ts`
```ts
component: {
name: "refresh-input",
action: "GenerateAccount",
buttonText: "生成ACME账号",
successMessage: "ACME账号已生成,请保存授权配置",
type: "textarea",
rows: 4,
}
```
## `remote-auto-complete`
Source: `packages/ui/certd-server/src/plugins/plugin-aliyun/plugin/deploy-to-apig/index.ts`
```ts
component: {
name: "remote-auto-complete",
}
```
## `remote-select`
Source: `packages/ui/certd-server/src/plugins/plugin-nginx-proxy-manager/plugins/plugin-deploy-to-proxy-hosts.ts`
```ts
component: {
name: "remote-select",
vModel: "value",
mode: "tags",
type: "plugin",
action: "onGetProxyHostOptions",
search: true,
pager: false,
single: false,
watches: ["certDomains", "accessId"],
}
```
## `remote-tree-select`
Source: `packages/ui/certd-server/src/plugins/plugin-tencent/plugin/refresh-cert/index.ts`
```ts
component: {
name: "remote-tree-select",
vModel: "value",
action: TencentRefreshCert.prototype.onGetRegionsTree.name,
pager: false,
search: false,
watches: ["certList"],
}
```
## `synology-device-id-getter`
Source: `packages/ui/certd-server/src/plugins/plugin-plus/synology/access.ts`
```ts
component: {
placeholder: "设备ID",
name: "synology-device-id-getter",
type: "access",
typeName: "synology",
}
```
@@ -1,25 +0,0 @@
# 本地历史记录
历史记录和开发临时文件只保存在 Codex/Trae 当前工作区的 `.tmp/online-plugin-dev/` 下,不调用 Certd 后端历史接口。
目录格式:
```text
.tmp/online-plugin-dev/
history/
plugin-12/
2026-08-02T12-30-00-before-edit.yaml
2026-08-02T12-30-00-change.md
work/
plugin-12.yaml
plugin-12-content.js
```
要求:
- 修改前保存完整 YAML。
- 临时 YAML、脚本草稿、调试记录都放到 `.tmp/online-plugin-dev/` 下,不散落到项目目录。
- `change.md` 只记录插件 ID、版本、时间和脱敏修改摘要。
- 不保存 Token、证书、私钥、Cookie、环境变量和真实授权值。
- 恢复历史版本前先备份当前 YAML。
- 恢复后通过 `/scoped/sys/ai/plugin/import` 写回 Certd。
@@ -1,55 +0,0 @@
## 在线插件 YAML
在线插件始终以一个完整 YAML 文档传递、编辑、导入和导出。脚本源码必须放在顶层 `content` 字段中,不要输出独立的 `.ts` 文件,也不要使用 JSON Patch。
常用字段:
```yaml
name: DemoTask
author: demo
title: Demo 任务
desc: 插件说明
icon: clarity:plugin-line
pluginType: task
group: other
version: 1.0.0
input:
cert:
title: 域名证书
required: true
component:
name: cert-select
output: {}
dependPlugins: []
dependPackages: []
default: {}
content: |
const { AbstractTaskPlugin } = await _ctx.import("@certd/pipeline")
const { DemoAccess } = await _ctx.import("/@/plugins/plugin-lib/demo/access/index.js")
_ctx.logger.info("DemoAccess:", DemoAccess)
return class DemoTask extends AbstractTaskPlugin {
async execute() {
this.logger.info("执行成功")
}
}
```
### `content` 规则
- 统一使用 `await _ctx.import(...)` 加载模块。
- 使用 `_ctx.import("/@/...")` 以绝对路径加载 `certd-server/src/` 下的模块,`/@` 代表 `certd-server/src` 根路径。
- 需要确认模块时使用 `_ctx.logger`,插件执行过程使用 `this.logger`
- 最后返回继承目标基类的 class。
- 不使用 `import``export`、装饰器或独立源码文件语法。
- 输入字段在 class 中声明为同名属性,并与 YAML 的 `input` 配置保持一致。
- HTTP 使用 `this.ctx.http`
- 不要使用 `console.log`
- 读取授权使用 `await this.getAccess(accessId)` 或目标基类规定的授权方式。
- 失败时抛出 `Error`,不要吞掉错误。
### 编辑规则
- 修改已有插件时保留 `name``author``pluginType` 和已有兼容字段。
- 只修改需求涉及的字段,避免删除未知的 YAML 字段。
- 脚本过长时仍放在同一个 `content` block scalar 中。
- 提交前检查 YAML 可解析、`content` 非空、版本和插件类型没有被意外修改。
@@ -1,59 +0,0 @@
---
name: certd-online-access-plugin-dev
description: 用于开发 Certd 在线 Access 插件。输出完整 YAMLcontent 中返回继承 BaseAccess 的 class,并在 input 中声明授权字段。
---
# 在线 Access 插件
读取父 Skill 的 `references/online-yaml-format.md`。不要沿用旧版 `@IsAccess``@AccessInput` 装饰器和独立 TypeScript 文件。
## 输出结构
- `pluginType` 使用 `access`
- `input` 中声明用户需要填写的授权字段。
- 敏感字段在 input 中设置加密或密码类组件。
- `content` 中实现授权 class 和 API 方法。
## `content` 模板
```javascript
const { BaseAccess } = await _ctx.import("@certd/pipeline")
return class DemoAccess extends BaseAccess {
demoKeyId
demoKeySecret
async onTestRequest() {
await this.getDomainList({ searchKey: "" })
return "ok"
}
async getDomainList(req) {
this.logger.info("获取域名列表", { searchKey: req.searchKey })
const res = await this.ctx.http.request({
url: "https://api.example.com/domains",
method: "GET",
params: { keyword: req.searchKey },
})
if (res.error) {
throw new Error(`获取域名列表失败: ${res.message}`)
}
return {
total: res.data?.total || 0,
list: res.data?.list || [],
}
}
}
```
## 编写要求
- 统一使用 `await _ctx.import(...)` 加载模块。
- 使用 `_ctx.import("/@/...")` 通过绝对路径加载 `server/src/` 下的模块,`/@` 代表 `server/src` 根路径。
- 需要记录模块加载信息时使用 `_ctx.logger`,访问执行日志使用 `this.logger`
- 返回继承 `BaseAccess` 的 class,不使用装饰器。
- class 属性名必须与 YAML `input` 字段一致。
- 所有敏感授权值只通过 `this` 和 Certd 授权上下文使用,不打印真实值。
- `onTestRequest` 应调用实际 API 方法并在失败时抛出异常。
- 对外 API 方法应统一处理分页、错误和返回字段。
- 使用 `this.logger` 或框架提供的 logger,禁止 `console.log`
@@ -1,70 +0,0 @@
---
name: certd-online-dns-provider-dev
description: 用于开发 Certd 在线 DNS Provider 插件。输出完整 YAMLcontent 中返回继承 AbstractDnsProvider 的 class。
---
# 在线 DNS Provider 插件
读取父 Skill 的 `references/online-yaml-format.md`。不要沿用旧版 `@IsDnsProvider` 装饰器和独立 TypeScript 文件。
## 输出结构
- `pluginType` 使用 `dnsProvider`
- `input` 中配置授权选择、域名或平台所需的参数。
- `content` 中实现创建和删除 DNS 记录的 class。
## `content` 模板
```javascript
const { AbstractDnsProvider } = await _ctx.import("@certd/pipeline")
const { DemoAccess } = await _ctx.import("/@/plugins/plugin-lib/demo/access/index.js")
_ctx.logger.info("DemoAccess:", DemoAccess)
return class DemoDnsProvider extends AbstractDnsProvider {
accessId
async onInstance() {
this.access = await this.getAccess(this.accessId)
}
async createRecord(options) {
const { fullRecord, value, type, domain } = options
this.logger.info("添加 DNS 记录", { fullRecord, type, domain })
const res = await this.ctx.http.request({
url: "https://api.example.com/dns/records",
method: "POST",
data: { fullRecord, value, type, domain },
})
if (res.error) {
throw new Error(`创建 DNS 记录失败: ${res.message}`)
}
return res.data
}
async removeRecord(options) {
const { fullRecord, value, domain } = options.recordReq
const res = await this.ctx.http.request({
url: "https://api.example.com/dns/records",
method: "DELETE",
data: { fullRecord, value, domain },
})
if (res.error) {
this.logger.warn("删除 DNS 记录失败", res.message)
return
}
this.logger.info("删除 DNS 记录成功", fullRecord)
}
}
```
## 编写要求
- 统一使用 `await _ctx.import(...)` 加载模块。
- 使用 `_ctx.import("/@/...")` 通过绝对路径加载 `server/src/` 下的模块,`/@` 代表 `server/src` 根路径。
- 需要记录模块加载信息时使用 `_ctx.logger`
- 返回继承 `AbstractDnsProvider` 的 class。
- `createRecord` 必须返回删除时需要的记录信息。
- `removeRecord` 使用 `options.recordReq``options.recordRes`
- 只处理业务 API 所需的 TXT 记录参数,不在日志中输出授权密钥。
- 网络失败、授权失败和 API 业务失败要有明确日志;创建失败必须抛出异常。
- 保持创建和删除幂等,避免清理失败阻断无关流程。
@@ -1,54 +0,0 @@
---
name: certd-online-task-plugin-dev
description: 用于开发 Certd 在线 Task 插件。输出完整 YAML,脚本源码放在 content 字段中,继承 AbstractTaskPlugin 并返回插件 class。
---
# 在线 Task 插件
读取父 Skill 的 `references/online-yaml-format.md`。在线插件不是原来的装饰器源码文件模式。
## 输出结构
- `pluginType` 使用 `task`
- 保留或填写 `name``author``title``desc``icon``group``version`
- 输入配置放在 YAML 的 `input` 字段。
- 执行脚本放在 YAML 顶层 `content` 字段。
## `content` 模板
```javascript
const { AbstractTaskPlugin } = await _ctx.import("@certd/pipeline")
const { DemoAccess } = await _ctx.import("/@/plugins/plugin-lib/demo/access/index.js")
_ctx.logger.info("DemoAccess:", DemoAccess)
return class DemoTask extends AbstractTaskPlugin {
cert
certDomains
accessId
async execute() {
const access = await this.getAccess(this.accessId)
this.logger.info("开始执行任务", { access })
const res = await this.ctx.http.request({
url: "https://api.example.com",
})
if (res.error) {
throw new Error(`任务执行失败: ${res.message}`)
}
this.logger.info("执行成功")
}
}
```
## 编写要求
- 统一用 `await _ctx.import(...)` 加载模块。
- 使用 `_ctx.import("/@/...")` 通过绝对路径加载 `server/src/` 下的模块,`/@` 代表 `server/src` 根路径。
- 需要记录模块加载信息时使用 `_ctx.logger`
- 返回继承 `AbstractTaskPlugin` 的 class,不写 `export class`
- class 属性名必须对应 `input` 配置的字段名。
-`this.logger` 记录关键步骤。
-`this.ctx.http` 请求远程 API,用 `this.getAccess` 获取授权。
- 外部 API 返回失败或业务失败时抛出异常。
- 对重复执行保持幂等,避免把真实 Token、证书和私钥写入日志。
- 修改完成后把整个 YAML 通过 Certd `/scoped/sys/ai/plugin/import` 保存。
-14
View File
@@ -87,20 +87,6 @@
"plus_use_prod": "false", "plus_use_prod": "false",
"PLUS_SERVER_BASE_URL": "http://127.0.0.1:11007" "PLUS_SERVER_BASE_URL": "http://127.0.0.1:11007"
} }
},
{
"name": "server-local-comm",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/packages/ui/certd-server",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev-localcomm"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": {
"plus_use_prod": "false",
"PLUS_SERVER_BASE_URL": "http://127.0.0.1:11007"
}
} }
], ],
"compounds": [ "compounds": [
-21
View File
@@ -76,7 +76,6 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。 - 优先沿用现有模块、插件、service、页面模式;不要为形式上的复用制造过度抽象。
- 代码可读性优先于短写法。复杂条件、三元表达式、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。 - 代码可读性优先于短写法。复杂条件、三元表达式、链式调用、内联对象和多层 helper 调用要拆成命名清晰的中间变量或小方法。
- 方法调用链不要直接塞进另一个方法参数;先用有意义的局部变量承接返回值,再传入下一步。 - 方法调用链不要直接塞进另一个方法参数;先用有意义的局部变量承接返回值,再传入下一步。
- 不要在单一表达式内嵌套分支、对象构造与方法调用。优先使用清晰的 `if/else` 分支;仅在确实能降低复杂度时才提取有意义的中间变量,避免为拆分而增加阅读跳转。
- 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。 - 注释优先使用中文,尤其是业务规则、兼容逻辑、协议细节和隐藏风险;文件已有英文风格或引用外部术语时可保持一致。
- 遵守 DRY 和单一职责;第三次出现的业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑,应优先抽成合适 helper 或 service 方法。 - 遵守 DRY 和单一职责;第三次出现的业务规则、字段转换、权限判断、Repository 选择、事务传播、金额计算等逻辑,应优先抽成合适 helper 或 service 方法。
@@ -85,7 +84,6 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 务必写单元测试,覆盖主要业务逻辑。 - 务必写单元测试,覆盖主要业务逻辑。
- 实现新功能或修复行为缺陷前,优先补单元测试并先确认红灯,再实现并跑聚焦验证。 - 实现新功能或修复行为缺陷前,优先补单元测试并先确认红灯,再实现并跑聚焦验证。
- 单元测试应优先直接测试原始业务方法,不要为了方便测试而抽取没有业务价值的 helper;数据库、RPC 和其他外部依赖可以使用 mock 隔离。
- 确实不适合先写测试时,在回复中说明原因和替代验证方式。 - 确实不适合先写测试时,在回复中说明原因和替代验证方式。
- 后补单元测试时,按正确行为写预期;若红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。 - 后补单元测试时,按正确行为写预期;若红灯需要修改既有实现,先向用户确认这是 bug 还是既有需求,避免未经确认改变行为。
- 后端纯单测放在 `src/**/*.test.ts`,尽量与被测文件相邻;`test:unit` 只跑这些文件,构建/打包应排除 `*.test.ts` - 后端纯单测放在 `src/**/*.test.ts`,尽量与被测文件相邻;`test:unit` 只跑这些文件,构建/打包应排除 `*.test.ts`
@@ -106,7 +104,6 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 只有需要事务传播时才定义 `ctx`;普通查询、纯函数和简单私有方法继续使用明确参数。 - 只有需要事务传播时才定义 `ctx`;普通查询、纯函数和简单私有方法继续使用明确参数。
- 需要按事务上下文取 Repository 时,用 `BaseService.getRepo(ctx, EntityClass)` - 需要按事务上下文取 Repository 时,用 `BaseService.getRepo(ctx, EntityClass)`
- 需要“有事务则复用、无事务则开启”时,用 `BaseService.transactionWithCtx(ctx, callback)` - 需要“有事务则复用、无事务则开启”时,用 `BaseService.transactionWithCtx(ctx, callback)`
- 基础 CRUD 数据访问优先复用 `BaseService``find``findOne``list``page``update``deleteWhere` 等方法;不要从 `repository.createQueryBuilder()` 开始重复实现完整查询或更新。仅将关键词组合筛选、联表、业务排序等基类无法表达的部分放入 `list/page``buildQuery`
- 拼接可选 `projectId` 查询条件时,**必须**使用 `BaseService.buildUserProjectQuery(userId, projectId)`,禁止直接写 `{ userId, projectId }`。因为 `projectId` 可能为 `null`/`undefined`,直接放入查询会生成错误的 `WHERE projectId = NULL` 条件。 - 拼接可选 `projectId` 查询条件时,**必须**使用 `BaseService.buildUserProjectQuery(userId, projectId)`,禁止直接写 `{ userId, projectId }`。因为 `projectId` 可能为 `null`/`undefined`,直接放入查询会生成错误的 `WHERE projectId = NULL` 条件。
- `ctx` 类型复用 `BaseService` 导出的 `ServiceContext` - `ctx` 类型复用 `BaseService` 导出的 `ServiceContext`
- 新增 service 方法避免与 `BaseService` 方法签名冲突,例如不要用 `delete(id)` 覆盖 `delete(ids, where?)`;改用 `deleteById` 等具体名称。 - 新增 service 方法避免与 `BaseService` 方法签名冲突,例如不要用 `delete(id)` 覆盖 `delete(ids, where?)`;改用 `deleteById` 等具体名称。
@@ -142,9 +139,6 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
- 列表管理、后台管理、记录查询、CRUD 表格页面优先使用 Fast Crud;开发或重构前读 `.trae/skills/fast-crud-page-dev/SKILL.md` - 列表管理、后台管理、记录查询、CRUD 表格页面优先使用 Fast Crud;开发或重构前读 `.trae/skills/fast-crud-page-dev/SKILL.md`
- 只有轻量只读展示、强交互自定义界面或既有页面模式明显不适合 Fast Crud 时,才手写 `a-table` / 自定义列表,并在回复中说明。 - 只有轻量只读展示、强交互自定义界面或既有页面模式明显不适合 Fast Crud 时,才手写 `a-table` / 自定义列表,并在回复中说明。
- 内嵌 Fast Crud 时,外层必须有稳定高度或完整 `flex: 1; min-height: 0` 链路。 - 内嵌 Fast Crud 时,外层必须有稳定高度或完整 `flex: 1; min-height: 0` 链路。
- 前端组件样式统一写在 `<style>` / Less / CSS 文件里,通过样式名映射到元素;尽量不要在元素上直接写 `style`
- 每个组件都要有一个稳定的根样式名,并把组件下方样式全部包在该根样式名内;尽量不要使用 `scoped`
- 可复用的公共样式名放在 `packages/ui/certd-client/src/style` 下维护,优先使用 `cd-` 前缀,避免散落在业务组件里重复定义。
- 后台管理列表展示或筛选用户字段时,优先参考 `packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx``userId` 字段模式,用 `table-select` + `/sys/authority/user/getSimpleUserByIds` 字典回显和搜索。 - 后台管理列表展示或筛选用户字段时,优先参考 `packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx``userId` 字段模式,用 `table-select` + `/sys/authority/user/getSimpleUserByIds` 字典回显和搜索。
- 对话框里只做确认可用 `Modal.confirm`;有字段输入、表单校验或提交字段时,必须用 `useFormDialog` / `openFormDialog` - 对话框里只做确认可用 `Modal.confirm`;有字段输入、表单校验或提交字段时,必须用 `useFormDialog` / `openFormDialog`
@@ -223,18 +217,3 @@ Certd 是可私有化部署的 SSL/TLS 证书自动化管理平台,提供 Web
### 旧版数据兼容 ### 旧版数据兼容
- 新增插件参数时,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default``custom`,需要在使用时判空,走旧版逻辑。 - 新增插件参数时,必须要考虑旧版数据兼容,比如新增一个deployType参数,有两种值:`default``custom`,需要在使用时判空,走旧版逻辑。
## 前端路由与国际化
- 路由 `meta.title`**i18n 国际化 key**,必须在 `src/locales/langs/zh-CN/``src/locales/langs/en-US/` 对应的模块文件中添加翻译。
- 示例:路由 `title: "certd.auditLog"` 需要在中英 locales 文件中有对应 key`"certd.auditLog": "操作日志"` / `"certd.auditLog": "Audit Log"`)。
- 菜单通过路由自动生成,需设置 `meta.isMenu: true` 才会出现在左侧菜单。
- Plus 版功能菜单需设置 `meta.show: () => { const settingStore = useSettingStore(); return settingStore.isPlus; }`
## 审计日志
- 审计日志是 Plus 版功能,非 Plus 版不会写入。
- Controller 继承 `BaseController`,通过 `this.auditLog({ content: "xxx" })` 记录日志。
- Controller 中的 `@Post("/add", { summary: "xxxx" })`, 这个summary是必须要的,他是日志action字段的来源
- `getAuditType()` 返回类型常量,中间件自动从 ctx.path 判定 scope`/api/sys/` → system,其他 → user)。
- 操作日志有系统级(scope=system)和用户级(scope=user)区分。
+16 -39
View File
@@ -1,4 +1,4 @@
# Certd # Certd
中文 | [English](./README_en.md) 中文 | [English](./README_en.md)
@@ -105,56 +105,33 @@ https://certd.handfree.work/
#### Docker镜像说明: #### Docker镜像说明:
##### 1. 镜像地址格式: **镜像版本:**
```
registry.cn-shenzhen.aliyuncs.com/certd/certd:[version-][system-][latest/stable]
------------ ↑ 镜像地址 ------------- ↑ 镜像名 -- ↑指定版本- ↑基础系统- ↑最新版本类型
```
##### 2. 版本标签:
**最新版本标签:** | 标签 | 指定版本 | 基础系统 | 说明 |
| 版本 | 标签 | 说明 |
| --- | --- | --- |
| 最新预览版【默认】 | `certd:latest` | 指向最新开发版本,包含最新功能,但稳定性不如稳定版 |
| 最新稳定版 | `certd:stable` | 指向经过充分测试的生产就绪版本,推荐生产环境使用 |
**系统分支版本:**
> 根据基础镜像不同,分为如下三个分支版本,没有特殊需求选择默认的即可(他们功能是一样的)
| 系统版本 | 版本标签 | 基础系统 | 说明 | 稳定版标签 |
| --- | --- | --- | --- | --- |
| alpine【默认】 | `certd:latest` | Alpine Linux | 默认版本,镜像体积小 | `certd:stable` |
| slim | `certd:slim` | Debian slim | 基于glibcdns解析兼容性好 | `certd:slim-stable` |
| armv7 | `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:armv7-stable` |
##### 2. 镜像地址:
| 镜像仓库 | 最新预览版 | slim | armv7 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 阿里云【默认】 | `registry.cn-shenzhen.aliyuncs.com/certd/certd:latest` | `certd:slim` | `certd:armv7` | | `latest` | `[version]` | Alpine Linux | 默认版本,镜像体积小 |
| Docker Hub | `greper/certd:latest` | `certd:slim` | | `slim` | `[version]-slim` | Debian slim | 基于glibcdns解析兼容性好(可能需要配置security_opt -seccomp=unconfined |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `certd:slim` | `certd:armv7` | | `armv7` | `[version]-armv7` | Alpine Linux | ARMv7 架构专用版本 |
> 注意: **镜像地址:**
> 1. 后面的各个版本省略了镜像地址,使用时需要将镜像地址拼接完整。
> 2. 稳定版在后面加 `-stable` 即可。
> 3. 如需指定具体的版本号,在冒号后面加 `version-`即可,例如 `certd:1.42.1-stable`。
| 镜像仓库 | latest | slim | armv7 |
| --- | --- | --- | --- |
| 阿里云 | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7` |
| Docker Hub | `greper/certd:latest` | `greper/certd:slim` | `greper/certd:armv7` |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `ghcr.io/certd/certd:slim` | `ghcr.io/certd/certd:armv7` |
##### 3. 镜像构建说明: > 带版本号的标签请将 `latest` / `slim` / `armv7` 替换为 `[version]` / `[version]-slim` / `[version]-armv7`
- 镜像构建通过`Actions`自动执行,过程公开透明,请放心使用 - 镜像构建通过`Actions`自动执行,过程公开透明,请放心使用
- [点我查看预览版构建日志](https://github.com/certd/certd/actions/workflows/release-image.yml) - [点我查看镜像构建日志](https://github.com/certd/certd/actions/workflows/build-image.yml)
- [点我查看稳定版发布日志](https://github.com/certd/certd/actions/workflows/stable-release.yml)
![](./docs/images/action/action-build.jpg) ![](./docs/images/action/action-build.jpg)
##### 4. 安全注意事项 > 注意
>
> - 本应用存储的证书、授权信息等属于高度敏感数据,请做好安全防护 > - 本应用存储的证书、授权信息等属于高度敏感数据,请做好安全防护
> - 请务必使用HTTPS协议访问本应用,避免被中间人攻击 > - 请务必使用HTTPS协议访问本应用,避免被中间人攻击
> - 请务必使用web应用防火墙防护本应用,防止XSS、SQL注入等攻击 > - 请务必使用web应用防火墙防护本应用,防止XSS、SQL注入等攻击
+13 -36
View File
@@ -1,4 +1,4 @@
# Certd # Certd
[中文](./README.md) | English [中文](./README.md) | English
@@ -95,44 +95,21 @@ You can choose one of the following deployment methods based on your needs:
#### Docker Image Information: #### Docker Image Information:
**Release channels:** - Domestic Image Addresses:
- `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest`
- `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7`, `[version]-armv7`
- DockerHub Addresses:
- `https://hub.docker.com/r/greper/certd`
- `greper/certd:latest`
- `greper/certd:armv7`, `greper/certd:[version]-armv7`
- GitHub Packages Addresses:
| Channel | Description | - `ghcr.io/certd/certd:latest`
| --- | --- | - `ghcr.io/certd/certd:armv7`, `ghcr.io/certd/certd:[version]-armv7`
| `stable` / `slim-stable` | **Stable version**, production-ready and fully tested, recommended for production environments |
| `latest` / `slim` / `armv7` | **Preview version**, latest development build with newest features but potentially less stable |
**Image tags:**
| Channel | Tag | Versioned Tag | Base System | Description |
| --- | --- | --- | --- | --- |
| **Stable** | `stable` | `[version]-stable` | Alpine Linux | Recommended for production |
| | `slim-stable` | `[version]-slim-stable` | Debian slim | Better DNS resolution compatibility |
| **Preview** | `latest` | `[version]` | Alpine Linux | Default, small image size |
| | `slim` | `[version]-slim` | Debian slim | Better DNS resolution compatibility |
| | `armv7` | `[version]-armv7` | Alpine Linux | ARMv7 architecture |
**Stable version image addresses:**
| Registry | `stable` | `slim-stable` |
| --- | --- | --- |
| Aliyun | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:stable` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim-stable` |
| Docker Hub | `greper/certd:stable` | `greper/certd:slim-stable` |
| GitHub Packages | `ghcr.io/certd/certd:stable` | `ghcr.io/certd/certd:slim-stable` |
**Preview version image addresses:**
| Registry | `latest` | `slim` | `armv7` |
| --- | --- | --- | --- |
| Aliyun | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:slim` | `registry.cn-shenzhen.aliyuncs.com/handsfree/certd:armv7` |
| Docker Hub | `greper/certd:latest` | `greper/certd:slim` | `greper/certd:armv7` |
| GitHub Packages | `ghcr.io/certd/certd:latest` | `ghcr.io/certd/certd:slim` | `ghcr.io/certd/certd:armv7` |
> For versioned tags, replace tag name with `[version]-tag`, e.g. replace `stable` with `[version]-stable`
- Images are built automatically by `Actions`, with a transparent process. Please use them with confidence. - Images are built automatically by `Actions`, with a transparent process. Please use them with confidence.
- [Click here to view preview version build logs](https://github.com/certd/certd/actions/workflows/release-image.yml) - [Click here to view image build logs](https://github.com/certd/certd/actions/workflows/build-image.yml)
- [Click here to view stable version release logs](https://github.com/certd/certd/actions/workflows/stable-release.yml)
![](./docs/images/action/action-build.jpg) ![](./docs/images/action/action-build.jpg)
> Note: > Note:
+1 -5
View File
@@ -2,13 +2,9 @@ version: '3.3' # 兼容旧版docker-compose
services: services:
certd: certd:
# 镜像 # ↓↓↓↓↓ ---- 镜像版本号,建议改成固定版本号,例如:certd:1.29.0 # 镜像 # ↓↓↓↓↓ ---- 镜像版本号,建议改成固定版本号,例如:certd:1.29.0
image: registry.cn-shenzhen.aliyuncs.com/certd/certd:latest image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
# image: ghcr.io/certd/certd:latest # --------- 如果 报镜像not found,可以尝试其他镜像源 # image: ghcr.io/certd/certd:latest # --------- 如果 报镜像not found,可以尝试其他镜像源
# image: greper/certd:latest # image: greper/certd:latest
# --------- 生产建议使用稳定版, latest改成stable即可
# image: registry.cn-shenzhen.aliyuncs.com/certd/certd:stable
# security_opt: # --------- 如果slim镜像下启动报错,尝试去掉这两行注释 # security_opt: # --------- 如果slim镜像下启动报错,尝试去掉这两行注释
# - seccomp=unconfined # 解决slim镜像下WorkerThreadsTaskRunner::DelayedTaskScheduler::Start() 报错问题 # - seccomp=unconfined # 解决slim镜像下WorkerThreadsTaskRunner::DelayedTaskScheduler::Start() 报错问题
container_name: certd # 容器名 container_name: certd # 容器名
+6 -15
View File
@@ -6,22 +6,13 @@
Certd 提供多种 Docker 镜像版本,您可以根据需要选择: Certd 提供多种 Docker 镜像版本,您可以根据需要选择:
**最新版本:** | 版本标签 | 基础系统 | 说明 |
| --- | --- | --- | --- |
| `latest` / `[version]` | Alpine Linux | 默认版本,镜像体积小 |
| `slim` / `[version]-slim` | Debian slim | glibc版本,dns解析兼容性更好(可能需要配置security_opt -seccomp=unconfined|
| `armv7` / `[version]-armv7` | Alpine Linux | ARMv7 架构专用版本 |
| 版本 | 标签 | 说明 | > 如果您不确定使用哪个版本,请使用默认的 `latest` 版本。
| --- | --- | --- |
| 预览版【默认】 | `certd:latest` | 指向最新开发版本,包含最新功能,但稳定性不如稳定版 |
| 稳定版 | `certd:stable` | 指向经过充分测试的生产就绪版本,推荐生产环境使用 |
**系统版本分支:**
| 分支版本标签 | 基础系统 | 说明 | 指定版本 | 稳定版 |
| --- | --- | --- | --- | --- |
| `certd:latest` 【默认】 | Alpine Linux | 默认版本,镜像体积小 | `certd:[version]` | `certd:[version-]stable` |
| `certd:slim` | Debian slim | glibc版本,dns解析兼容性更好(可能需要配置security_opt -seccomp=unconfined| `certd:[version-]slim` | `certd:[version-]slim-stable` |
| `certd:armv7` | Alpine Linux | ARMv7 架构专用版本 | `certd:[version]-armv7` | `certd:[version-]armv7-ststable` |
> 如果您不确定使用哪个版本,请使用默认的 `certd:latest` 版本。
### 一键脚本安装(推荐) ### 一键脚本安装(推荐)
+1 -3
View File
@@ -45,9 +45,7 @@
"publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js", "publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js",
"publish_to_gitee": "node --experimental-json-modules ./scripts/publish-gitee.js", "publish_to_gitee": "node --experimental-json-modules ./scripts/publish-gitee.js",
"publish_to_github": "node --experimental-json-modules ./scripts/publish-github.js", "publish_to_github": "node --experimental-json-modules ./scripts/publish-github.js",
"get_version": "node --experimental-json-modules ./scripts/version.js", "get_version": "node --experimental-json-modules ./scripts/version.js"
"stable": "node ./scripts/stable.js",
"set-release-stable": "node ./scripts/set-release-stable.js"
}, },
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -50,7 +50,7 @@
"scripts": { "scripts": {
"before-build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true});\"", "before-build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true});\"",
"build": "npm run before-build && tsc -p tsconfig.build.json --skipLibCheck", "build": "npm run before-build && tsc -p tsconfig.build.json --skipLibCheck",
"lint": "eslint --fix \"src/**/*.ts\" \"types/**/*.ts\"", "lint": "eslint \"src/**/*.ts\" \"types/**/*.ts\"",
"lint-types": "tsd --files \"types/index.test-d.ts\"", "lint-types": "tsd --files \"types/index.test-d.ts\"",
"prepublishOnly": "npm run build", "prepublishOnly": "npm run build",
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"", "test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
+1 -1
View File
@@ -17,7 +17,7 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "tsc --skipLibCheck --watch", "compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix --ext .ts src" "lint": "eslint --fix"
}, },
"dependencies": { "dependencies": {
"async-lock": "^1.4.1", "async-lock": "^1.4.1",
+2 -5
View File
@@ -11,12 +11,9 @@ export class LocalCache<V = any> {
cache: Map<string, { value: V; expiresAt: number }>; cache: Map<string, { value: V; expiresAt: number }>;
constructor(opts: { clearInterval?: number } = {}) { constructor(opts: { clearInterval?: number } = {}) {
this.cache = new Map(); this.cache = new Map();
const intervalId = setInterval( const intervalId = setInterval(() => {
() => {
this.clearExpires(); this.clearExpires();
}, }, opts.clearInterval ?? 5 * 60 * 1000);
opts.clearInterval ?? 5 * 60 * 1000
);
intervalId.unref?.(); intervalId.unref?.();
} }
+2 -2
View File
@@ -1,4 +1,4 @@
export function isDev() { export function isDev() {
const nodeEnv = process.env.NODE_ENV || "dev"; const nodeEnv = process.env.NODE_ENV || 'dev';
return nodeEnv === "development" || nodeEnv.includes("local") || nodeEnv.startsWith("dev"); return nodeEnv === 'development' || nodeEnv.includes('local') || nodeEnv.startsWith('dev');
} }
+2 -2
View File
@@ -1,8 +1,8 @@
import fs from "fs"; import fs from 'fs';
function getFileRootDir(rootDir?: string) { function getFileRootDir(rootDir?: string) {
if (rootDir == null) { if (rootDir == null) {
const userHome = process.env.HOME || process.env.USERPROFILE; const userHome = process.env.HOME || process.env.USERPROFILE;
rootDir = userHome + "/.certd/storage/"; rootDir = userHome + '/.certd/storage/';
} }
if (!fs.existsSync(rootDir)) { if (!fs.existsSync(rootDir)) {
+1 -1
View File
@@ -1,2 +1,2 @@
import mitt from "mitt"; import mitt from 'mitt';
export const mitter = mitt(); export const mitter = mitt();
+1 -1
View File
@@ -18,7 +18,7 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "tsc --skipLibCheck --watch", "compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix --ext .ts src" "lint": "eslint --fix"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.42.6", "@certd/basic": "^1.42.6",
+1 -6
View File
@@ -389,9 +389,6 @@ export class Executor {
}; };
await instance.setCtx(taskCtx); await instance.setCtx(taskCtx);
if (!(instance instanceof AbstractTaskPlugin)) {
throw new Error(`插件类型错误:${step.type}不是AbstractTaskPlugin的实例`);
}
await instance.onInstance(); await instance.onInstance();
const result = await instance.execute(); const result = await instance.execute();
//执行结果处理 //执行结果处理
@@ -401,7 +398,6 @@ export class Executor {
} }
//输出上下文变量到output context //输出上下文变量到output context
forEach(define.output, (item: any, key: any) => { forEach(define.output, (item: any, key: any) => {
// @ts-ignore
step.status!.output[key] = instance[key]; step.status!.output[key] = instance[key];
// const stepOutputKey = `step.${step.id}.${key}`; // const stepOutputKey = `step.${step.id}.${key}`;
// this.runtime.context[stepOutputKey] = instance[key]; // this.runtime.context[stepOutputKey] = instance[key];
@@ -415,8 +411,7 @@ export class Executor {
merge(vars, instance._result.pipelineVars); merge(vars, instance._result.pipelineVars);
await this.pipelineContext.setObj("vars", vars); await this.pipelineContext.setObj("vars", vars);
} }
// @ts-ignore if (Object.keys(instance._result.pipelinePrivateVars).length > 0) {
if (Object.keys(instance._result?.pipelinePrivateVars).length > 0) {
// 判断 pipelineVars 有值时更新 // 判断 pipelineVars 有值时更新
let vars = await this.pipelineContext.getObj("privateVars"); let vars = await this.pipelineContext.getObj("privateVars");
vars = vars || {}; vars = vars || {};
@@ -568,18 +568,11 @@ export class RuntimeDepsService {
const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env: tryEnv }); const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env: tryEnv });
if (result.code === 0) { if (result.code === 0) {
this.writeInstallState(statePath, { installedAt: new Date().toISOString(), registryUrl: tryUrl, dependenciesHash, nodeVersion: process.version, pnpmVersion, lockFileExists: fs.existsSync(lockPath) }); this.writeInstallState(statePath, { installedAt: new Date().toISOString(), registryUrl: tryUrl, dependenciesHash, nodeVersion: process.version, pnpmVersion, lockFileExists: fs.existsSync(lockPath) });
log.info(`${result.stdout?.slice(-2000) || "无npm安装日志输出"}`);
log.info("第三方依赖安装完成"); log.info("第三方依赖安装完成");
return { registryUrl: tryUrl, packageJsonPath }; return { registryUrl: tryUrl, packageJsonPath };
} }
const errOutput = (result.stderr || "").trim(); lastError = result.stderr || result.stdout || "unknown error";
const outOutput = (result.stdout || "").trim(); log.warn?.(`镜像 ${tryUrl || "默认"} 安装失败${urlsToTry.length > 1 ? ",尝试下一个镜像..." : ""}`);
lastError = errOutput || outOutput || "unknown error";
log.info(`镜像 ${tryUrl || "默认"} 安装失败,退出码: ${result.code}${urlsToTry.length > 1 ? ",尝试下一个镜像..." : ""}`);
log.info(` pnpm stderr: ${(errOutput || "无npm安装日志输出").slice(-2000)}`);
if (outOutput) {
log.info(` pnpm stdout: ${outOutput.slice(-2000)}`);
}
} }
this.writeInstallState(statePath, { this.writeInstallState(statePath, {
...currentState, ...currentState,
+1 -1
View File
@@ -15,7 +15,7 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "npm run build", "compile": "npm run build",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix --ext .ts src" "lint": "eslint --fix"
}, },
"dependencies": { "dependencies": {
"axios": "^1.9.0", "axios": "^1.9.0",
+1 -1
View File
@@ -18,7 +18,7 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "npm run build", "compile": "npm run build",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix --ext .ts src" "lint": "eslint --fix"
}, },
"dependencies": { "dependencies": {
"nanoid": "^5.0.7" "nanoid": "^5.0.7"
+1 -1
View File
@@ -1 +1 @@
export * from "./lib/iframe.client.js"; export * from './lib/iframe.client.js';
@@ -1,4 +1,4 @@
import { nanoid } from "nanoid"; import { nanoid } from 'nanoid';
export type IframeMessageData<T> = { export type IframeMessageData<T> = {
action: string; action: string;
@@ -29,12 +29,10 @@ export class IframeClient {
onError?: any; onError?: any;
handlers: Record<string, (data: IframeMessageData<any>) => Promise<void>> = {}; handlers: Record<string, (data: IframeMessageData<any>) => Promise<void>> = {};
private messageHandler: (event: MessageEvent<IframeMessageData<any>>) => Promise<void>;
constructor(iframe?: HTMLIFrameElement, onError?: (e: any) => void) { constructor(iframe?: HTMLIFrameElement, onError?: (e: any) => void) {
this.iframe = iframe; this.iframe = iframe;
this.onError = onError; this.onError = onError;
this.messageHandler = async (event: MessageEvent<IframeMessageData<any>>) => { window.addEventListener('message', async (event: MessageEvent<IframeMessageData<any>>) => {
const data = event.data; const data = event.data;
if (data.action) { if (data.action) {
console.log(`收到消息[isSub:${this.isInFrame()}]`, data); console.log(`收到消息[isSub:${this.isInFrame()}]`, data);
@@ -42,21 +40,20 @@ export class IframeClient {
const handler = this.handlers[data.action]; const handler = this.handlers[data.action];
if (handler) { if (handler) {
const res = await handler(data); const res = await handler(data);
if (data.id && data.action !== "reply") { if (data.id && data.action !== 'reply') {
await this.send("reply", res, data.id); await this.send('reply', res, data.id);
} }
} else { } else {
throw new Error(`action:${data.action} 未注册处理器,可能版本过低`); throw new Error(`action:${data.action} 未注册处理器,可能版本过低`);
} }
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
await this.send("reply", {}, data.id, 500, e.message); await this.send('reply', {}, data.id, 500, e.message);
} }
} }
}; });
window.addEventListener("message", this.messageHandler);
this.register("reply", async data => { this.register('reply', async data => {
const req = this.requestQueue[data.replyId!]; const req = this.requestQueue[data.replyId!];
if (req) { if (req) {
req.onReply(data); req.onReply(data);
@@ -64,20 +61,11 @@ export class IframeClient {
} }
}); });
} }
isInFrame() {
public destroy() {
window.removeEventListener("message", this.messageHandler);
this.requestQueue = {};
this.handlers = {};
}
public close() {
this.destroy();
}
public isInFrame() {
return window.self !== window.top; return window.self !== window.top;
} }
public register<T = any>(action: string, handler: (data: IframeMessageData<T>) => Promise<any>) { register<T = any>(action: string, handler: (data: IframeMessageData<T>) => Promise<any>) {
this.handlers[action] = handler; this.handlers[action] = handler;
} }
@@ -118,12 +106,12 @@ export class IframeClient {
console.log(`send message[isSub:${this.isInFrame()}]:`, reqMessageData); console.log(`send message[isSub:${this.isInFrame()}]:`, reqMessageData);
if (!this.iframe) { if (!this.iframe) {
if (!window.parent) { if (!window.parent) {
reject("当前页面不在 iframe 中"); reject('当前页面不在 iframe 中');
} }
window.parent.postMessage(reqMessageData, "*"); window.parent.postMessage(reqMessageData, '*');
} else { } else {
//子页面 //子页面
this.iframe.contentWindow?.postMessage(reqMessageData, "*"); this.iframe.contentWindow?.postMessage(reqMessageData, '*');
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
+1 -1
View File
@@ -13,7 +13,7 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "npm run build", "compile": "npm run build",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix --ext .ts src" "lint": "eslint --fix"
}, },
"author": "", "author": "",
"license": "Apache", "license": "Apache",
+5 -5
View File
@@ -1,9 +1,9 @@
import jdCloud from './lib/core.js'; import jdCloud from "./lib/core.js";
import jdService from './lib/service.js'; import jdService from './lib/service.js'
import domainService from './repo/domainservice/v2/domainservice.js'; import domainService from './repo/domainservice/v2/domainservice.js'
import cdnService from './repo/cdn/v1/cdn.js'; import cdnService from './repo/cdn/v1/cdn.js'
import sslService from './repo/ssl/v1/ssl.js'; import sslService from './repo/ssl/v1/ssl.js'
export const JDCloud = jdCloud; export const JDCloud = jdCloud;
export const JDService = jdService; export const JDService = jdService;
export const JDDomainService = domainService; export const JDDomainService = domainService;
+1 -1
View File
@@ -18,7 +18,7 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "tsc --skipLibCheck --watch", "compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix --ext .ts src" "lint": "eslint --fix"
}, },
"dependencies": { "dependencies": {
"@certd/basic": "^1.42.6", "@certd/basic": "^1.42.6",
+1 -1
View File
@@ -19,7 +19,7 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "tsc --skipLibCheck --watch", "compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix --ext .ts src" "lint": "eslint --fix"
}, },
"keywords": [], "keywords": [],
"author": "greper", "author": "greper",
@@ -1,34 +0,0 @@
/// <reference types="mocha" />
import assert from "node:assert/strict";
import { AuditLogContext } from "./audit.js";
// AuditLog decorator and getAuditLogOptions are removed since auditLog()
// now signals audit intent directly via ctx.auditLog.enabled
describe("AuditLogContext type", () => {
it("supports enabled flag", () => {
const ctx: AuditLogContext = {
type: "pipeline",
action: "删除流水线",
append: ["ID:5"],
content: "删除了流水线(ID:5)",
projectId: 3,
enabled: true,
};
assert.equal(ctx.enabled, true);
assert.equal(ctx.type, "pipeline");
assert.equal(ctx.content, "删除了流水线(ID:5)");
assert.equal(ctx.projectId, 3);
});
it("works with minimal fields", () => {
const ctx: AuditLogContext = {
enabled: true,
append: ["提交2条"],
};
assert.equal(ctx.enabled, true);
});
});
@@ -1,44 +0,0 @@
export type AuditLogOptions = {
type?: string;
action?: string;
content?: string;
template?: string;
disabled?: boolean;
};
export type AuditLogContext = {
type?: string;
action?: string;
append?: string | string[];
content?: string;
projectId?: number;
enabled?: boolean;
scope?: string;
userId?: number;
username?: string;
success?: boolean;
};
/** 审计日志方法的参数类型 */
export type AuditLogParam = {
type?: string;
action?: string;
content?: string;
append?: string | string[];
projectId?: number;
userId?: number;
username?: string;
};
/** AuditService.log() 参数类型 */
export type AuditLogWriteParam = {
userId: number;
type: string;
action: string;
content: string;
username?: string;
projectId?: number;
ipAddress?: string;
scope?: string;
success?: boolean;
};
@@ -3,7 +3,6 @@ import type { IMidwayContainer } from "@midwayjs/core";
import * as koa from "@midwayjs/koa"; import * as koa from "@midwayjs/koa";
import { Constants } from "./constants.js"; import { Constants } from "./constants.js";
import { isEnterprise } from "./mode.js"; import { isEnterprise } from "./mode.js";
import type { AuditLogContext, AuditLogParam } from "./audit.js";
export abstract class BaseController { export abstract class BaseController {
@Inject() @Inject()
@@ -128,43 +127,4 @@ export abstract class BaseController {
} }
return { projectId, userId }; return { projectId, userId };
} }
getAuditType(): string {
return "unknown";
}
auditLog(bean: AuditLogParam = {}) {
const auditLog = this.ensureAuditLogContext();
auditLog.enabled = true;
if (bean.userId != null) {
auditLog.userId = bean.userId;
}
if (bean.username != null) {
auditLog.username = bean.username;
}
if (bean.type != null) {
auditLog.type = bean.type;
}
if (bean.action != null) {
auditLog.action = bean.action;
}
if (bean.projectId != null) {
auditLog.projectId = bean.projectId;
}
if (bean.content) {
auditLog.content = bean.content;
}
if (bean.append) {
const items = Array.isArray(bean.append) ? bean.append : [bean.append];
const old = Array.isArray(auditLog.append) ? auditLog.append : auditLog.append ? [auditLog.append] : [];
auditLog.append = [...old, ...items].filter(item => item && String(item).trim());
}
}
private ensureAuditLogContext(): AuditLogContext {
if (!this.ctx.auditLog) {
this.ctx.auditLog = {};
}
return this.ctx.auditLog;
}
} }
@@ -102,18 +102,6 @@ export abstract class BaseService<T> {
}); });
} }
/**
* 按条件直接更新,不触发子类 update 的业务生命周期。
*/
async updateWhere(where: any, data: any) {
await this.getRepository().update(
{
...where,
},
data
);
}
/** /**
* 删除 * 删除
* @param ids 删除的ID集合 如:[1,2,3] 或者 1,2,3 * @param ids 删除的ID集合 如:[1,2,3] 或者 1,2,3
+32 -32
View File
@@ -1,21 +1,21 @@
export const Constants = { export const Constants = {
dataDir: "./data", dataDir: './data',
role: { role: {
defaultUser: 3, defaultUser: 3,
}, },
per: { per: {
//无需登录 //无需登录
guest: "_guest_", guest: '_guest_',
//无需登录 //无需登录
anonymous: "_guest_", anonymous: '_guest_',
//无需登录,有 token 时解析当前用户 //无需登录,有 token 时解析当前用户
guestOptionalAuth: "_guestOptionalAuth_", guestOptionalAuth: '_guestOptionalAuth_',
//仅需要登录 //仅需要登录
authOnly: "_authOnly_", authOnly: '_authOnly_',
//仅需要登录 //仅需要登录
loginOnly: "_authOnly_", loginOnly: '_authOnly_',
open: "_open_", open: '_open_',
}, },
res: { res: {
serverError(message: string) { serverError(message: string) {
@@ -26,102 +26,102 @@ export const Constants = {
}, },
error: { error: {
code: 1, code: 1,
message: "Internal server error", message: 'Internal server error',
}, },
success: { success: {
code: 0, code: 0,
message: "success", message: 'success',
}, },
validation: { validation: {
code: 10, code: 10,
message: "参数错误", message: '参数错误',
}, },
needvip: { needvip: {
code: 88, code: 88,
message: "需要VIP", message: '需要VIP',
}, },
needsuite: { needsuite: {
code: 89, code: 89,
message: "需要购买或升级套餐", message: '需要购买或升级套餐',
}, },
loginError: { loginError: {
code: 2, code: 2,
message: "登录失败", message: '登录失败',
}, },
codeError: { codeError: {
code: 3, code: 3,
message: "验证码错误", message: '验证码错误',
}, },
auth: { auth: {
code: 401, code: 401,
message: "您还未登录或token已过期", message: '您还未登录或token已过期',
}, },
permission: { permission: {
code: 402, code: 402,
message: "您没有权限", message: '您没有权限',
}, },
param: { param: {
code: 400, code: 400,
message: "参数错误", message: '参数错误',
}, },
notFound: { notFound: {
code: 404, code: 404,
message: "页面/文件/资源不存在", message: '页面/文件/资源不存在',
}, },
preview: { preview: {
code: 10001, code: 10001,
message: "对不起,预览环境不允许修改此数据", message: '对不起,预览环境不允许修改此数据',
}, },
siteOff:{ siteOff:{
code: 10010, code: 10010,
message: "站点已关闭", message: '站点已关闭',
}, },
need2fa:{ need2fa:{
code: 10020, code: 10020,
message: "需要2FA认证", message: '需要2FA认证',
}, },
openKeyError: { openKeyError: {
code: 20000, code: 20000,
message: "ApiToken错误", message: 'ApiToken错误',
}, },
openKeySignError: { openKeySignError: {
code: 20001, code: 20001,
message: "ApiToken签名错误", message: 'ApiToken签名错误',
}, },
openKeyExpiresError: { openKeyExpiresError: {
code: 20002, code: 20002,
message: "ApiToken时间戳错误", message: 'ApiToken时间戳错误',
}, },
openKeySignTypeError: { openKeySignTypeError: {
code: 20003, code: 20003,
message: "ApiToken签名类型不支持", message: 'ApiToken签名类型不支持',
}, },
openParamError: { openParamError: {
code: 20010, code: 20010,
message: "请求参数错误", message: '请求参数错误',
}, },
openCertNotFound: { openCertNotFound: {
code: 20011, code: 20011,
message: "证书不存在", message: '证书不存在',
}, },
openCertNotReady: { openCertNotReady: {
code: 20012, code: 20012,
message: "证书还未生成", message: '证书还未生成',
}, },
openCertApplying: { openCertApplying: {
code: 20013, code: 20013,
message: "证书正在申请中,请稍后重新获取", message: '证书正在申请中,请稍后重新获取',
}, },
openDomainNoVerifier:{ openDomainNoVerifier:{
code: 20014, code: 20014,
message: "域名校验方式未配置", message: '域名校验方式未配置',
}, },
openEmailNotFound: { openEmailNotFound: {
code: 20021, code: 20021,
message: "用户邮箱还未配置", message: '用户邮箱还未配置',
}, },
}, },
systemUserId: 0, // 系统级别userid固定为0 systemUserId: 0, // 系统级别userid固定为0
enterpriseUserId: -1, // 企业模式用户id固定为-1 enterpriseUserId: -1 // 企业模式用户id固定为-1
}; };
@@ -1,20 +0,0 @@
import { createRequestParamDecorator } from "@midwayjs/core";
export const AuditLog = (opts: { type?: string; action?: string; content?: string; enabled?: boolean } = {}) => {
return createRequestParamDecorator(ctx => {
if (!ctx.auditLog) {
ctx.auditLog = {};
}
ctx.auditLog.enabled = opts.enabled !== false;
if (opts.type != null) {
ctx.auditLog.type = opts.type;
}
if (opts.action != null) {
ctx.auditLog.action = opts.action;
}
if (opts.content != null) {
ctx.auditLog.content = opts.content;
}
return ctx.auditLog;
});
};
@@ -1 +0,0 @@
export * from "./decoractor.js";
@@ -1,11 +1,11 @@
import { ALL, Body, Post, Query } from "@midwayjs/core"; import { ALL, Body, Post, Query } from '@midwayjs/core';
import { BaseController } from "./base-controller.js"; import { BaseController } from './base-controller.js';
export abstract class CrudController<T> extends BaseController { export abstract class CrudController<T> extends BaseController {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
abstract getService<T>(); abstract getService<T>();
@Post("/page") @Post('/page')
async page(@Body(ALL) body: any) { async page(@Body(ALL) body: any) {
const pageRet = await this.getService().page({ const pageRet = await this.getService().page({
query: body.query ?? {}, query: body.query ?? {},
@@ -16,7 +16,7 @@ export abstract class CrudController<T> extends BaseController {
return this.ok(pageRet); return this.ok(pageRet);
} }
@Post("/list") @Post('/list')
async list(@Body(ALL) body: any) { async list(@Body(ALL) body: any) {
const listRet = await this.getService().list({ const listRet = await this.getService().list({
query: body.query ?? {}, query: body.query ?? {},
@@ -25,33 +25,33 @@ export abstract class CrudController<T> extends BaseController {
return this.ok(listRet); return this.ok(listRet);
} }
@Post("/add") @Post('/add')
async add(@Body(ALL) bean: any) { async add(@Body(ALL) bean: any) {
delete bean.id; delete bean.id;
const id = await this.getService().add(bean); const id = await this.getService().add(bean);
return this.ok(id); return this.ok(id);
} }
@Post("/info") @Post('/info')
async info(@Query("id") id: number) { async info(@Query('id') id: number) {
const bean = await this.getService().info(id); const bean = await this.getService().info(id);
return this.ok(bean); return this.ok(bean);
} }
@Post("/update") @Post('/update')
async update(@Body(ALL) bean: any) { async update(@Body(ALL) bean: any) {
await this.getService().update(bean); await this.getService().update(bean);
return this.ok(null); return this.ok(null);
} }
@Post("/delete") @Post('/delete')
async delete(@Query("id") id: number) { async delete(@Query('id') id: number) {
await this.getService().delete([id]); await this.getService().delete([id]);
return this.ok(null); return this.ok(null);
} }
@Post("/deleteByIds") @Post('/deleteByIds')
async deleteByIds(@Body("ids") ids: number[]) { async deleteByIds(@Body('ids') ids: number[]) {
await this.getService().delete(ids); await this.getService().delete(ids);
return this.ok(null); return this.ok(null);
} }
@@ -1,17 +1,19 @@
import { Constants } from "../constants.js"; import { Constants } from '../constants.js';
import { BaseException } from "./base-exception.js"; import { BaseException } from './base-exception.js';
import { TextException } from "./common-exception.js"; import { TextException } from "./common-exception.js";
/** /**
* *
*/ */
export class AuthException extends BaseException { export class AuthException extends BaseException {
constructor(message?:string) { constructor(message?:string) {
super("AuthException", Constants.res.auth.code, message ? message : Constants.res.auth.message); super('AuthException', Constants.res.auth.code, message ? message : Constants.res.auth.message);
} }
} }
export class Need2FAException extends TextException { export class Need2FAException extends TextException {
constructor(message:string,data:any) { constructor(message:string,data:any) {
super("Need2FAException", Constants.res.need2fa.code, message ? message : Constants.res.need2fa.message, data); super('Need2FAException', Constants.res.need2fa.code, message ? message : Constants.res.need2fa.message,data);
} }
} }
@@ -3,7 +3,7 @@
*/ */
export class BaseException extends Error { export class BaseException extends Error {
code: number; code: number;
data?: any; data?:any
constructor(name: string, code: number, message: string ,data?:any) { constructor(name: string, code: number, message: string ,data?:any) {
super(message); super(message);
this.name = name; this.name = name;
@@ -1,10 +1,10 @@
import { Constants } from "../constants.js"; import { Constants } from '../constants.js';
import { BaseException } from "./base-exception.js"; import { BaseException } from './base-exception.js';
/** /**
* *
*/ */
export class CodeErrorException extends BaseException { export class CodeErrorException extends BaseException {
constructor(message) { constructor(message) {
super("CodeErrorException", Constants.res.codeError.code, message ? message : Constants.res.codeError.message); super('CodeErrorException', Constants.res.codeError.code, message ? message : Constants.res.codeError.message);
} }
} }
@@ -1,13 +1,12 @@
export * from "./auth-exception.js"; export * from './auth-exception.js';
export * from "./base-exception.js"; export * from './base-exception.js';
export * from "./permission-exception.js"; export * from './permission-exception.js';
export * from "./preview-exception.js"; export * from './preview-exception.js';
export * from "./validation-exception.js"; export * from './validation-exception.js';
export * from "./vip-exception.js"; export * from './vip-exception.js';
export * from "./common-exception.js"; export * from './common-exception.js';
export * from "./not-found-exception.js"; export * from './not-found-exception.js';
export * from "./param-exception.js"; export * from './param-exception.js';
export * from "./site-off-exception.js"; export * from './site-off-exception.js';
export * from "./login-error-exception.js"; export * from './login-error-exception.js'
export * from "./code-error-exception.js"; export * from './code-error-exception.js'
export * from "./non-retryable-exception.js";
@@ -1,14 +1,12 @@
import { Constants } from "../constants.js"; import { Constants } from '../constants.js';
import { BaseException } from "./base-exception.js"; import { BaseException } from './base-exception.js';
/** /**
* *
*/ */
export class LoginErrorException extends BaseException { export class LoginErrorException extends BaseException {
leftCount: number; leftCount: number;
userId?: number; constructor(message, leftCount: number) {
constructor(message, leftCount: number, userId?: number) { super('LoginErrorException', Constants.res.loginError.code, message ? message : Constants.res.loginError.message);
super("LoginErrorException", Constants.res.loginError.code, message ? message : Constants.res.loginError.message);
this.leftCount = leftCount; this.leftCount = leftCount;
this.userId = userId;
} }
} }
@@ -1,12 +0,0 @@
import assert from "assert";
import { NonRetryableException } from "./non-retryable-exception.js";
describe("NonRetryableException", () => {
it("sets the standard error name and message", () => {
const error = new NonRetryableException("cannot retry");
assert.equal(error.name, "NonRetryableException");
assert.equal(error.message, "cannot retry");
assert.equal(error instanceof Error, true);
});
});
@@ -1,6 +0,0 @@
export class NonRetryableException extends Error {
constructor(message: string) {
super(message);
this.name = "NonRetryableException";
}
}
@@ -1,10 +1,10 @@
import { Constants } from "../constants.js"; import { Constants } from '../constants.js';
import { BaseException } from "./base-exception.js"; import { BaseException } from './base-exception.js';
/** /**
* *
*/ */
export class NotFoundException extends BaseException { export class NotFoundException extends BaseException {
constructor(message) { constructor(message) {
super("NotFoundException", Constants.res.notFound.code, message ? message : Constants.res.notFound.message); super('NotFoundException', Constants.res.notFound.code, message ? message : Constants.res.notFound.message);
} }
} }
@@ -1,10 +1,10 @@
import { Constants } from "../constants.js"; import { Constants } from '../constants.js';
import { BaseException } from "./base-exception.js"; import { BaseException } from './base-exception.js';
/** /**
* *
*/ */
export class ParamException extends BaseException { export class ParamException extends BaseException {
constructor(message) { constructor(message) {
super("ParamException", Constants.res.param.code, message ? message : Constants.res.param.message); super('ParamException', Constants.res.param.code, message ? message : Constants.res.param.message);
} }
} }
@@ -1,10 +1,10 @@
import { Constants } from "../constants.js"; import { Constants } from '../constants.js';
import { BaseException } from "./base-exception.js"; import { BaseException } from './base-exception.js';
/** /**
* *
*/ */
export class PermissionException extends BaseException { export class PermissionException extends BaseException {
constructor(message?: string) { constructor(message?: string) {
super("PermissionException", Constants.res.permission.code, message ? message : Constants.res.permission.message); super('PermissionException', Constants.res.permission.code, message ? message : Constants.res.permission.message);
} }
} }
@@ -1,10 +1,14 @@
import { Constants } from "../constants.js"; import { Constants } from '../constants.js';
import { BaseException } from "./base-exception.js"; import { BaseException } from './base-exception.js';
/** /**
* *
*/ */
export class PreviewException extends BaseException { export class PreviewException extends BaseException {
constructor(message) { constructor(message) {
super("PreviewException", Constants.res.preview.code, message ? message : Constants.res.preview.message); super(
'PreviewException',
Constants.res.preview.code,
message ? message : Constants.res.preview.message
);
} }
} }
@@ -1,9 +1,9 @@
import { Constants } from "../constants.js"; import { Constants } from '../constants.js';
import { BaseException } from "./base-exception.js"; import { BaseException } from './base-exception.js';
/** /**
*/ */
export class SiteOffException extends BaseException { export class SiteOffException extends BaseException {
constructor(message) { constructor(message) {
super("SiteOffException", Constants.res.siteOff.code, message ? message : Constants.res.siteOff.message); super('SiteOffException', Constants.res.siteOff.code, message ? message : Constants.res.siteOff.message);
} }
} }
@@ -1,10 +1,10 @@
import { Constants } from "../constants.js"; import { Constants } from '../constants.js';
import { BaseException } from "./base-exception.js"; import { BaseException } from './base-exception.js';
/** /**
* *
*/ */
export class ValidateException extends BaseException { export class ValidateException extends BaseException {
constructor(message) { constructor(message) {
super("ValidateException", Constants.res.validation.code, message ? message : Constants.res.validation.message); super('ValidateException', Constants.res.validation.code, message ? message : Constants.res.validation.message);
} }
} }
@@ -1,16 +1,16 @@
import { Constants } from "../constants.js"; import { Constants } from '../constants.js';
import { BaseException } from "./base-exception.js"; import { BaseException } from './base-exception.js';
/** /**
* vip异常 * vip异常
*/ */
export class NeedVIPException extends BaseException { export class NeedVIPException extends BaseException {
constructor(message) { constructor(message) {
super("NeedVIPException", Constants.res.needvip.code, message ? message : Constants.res.needvip.message); super('NeedVIPException', Constants.res.needvip.code, message ? message : Constants.res.needvip.message);
} }
} }
export class NeedSuiteException extends BaseException { export class NeedSuiteException extends BaseException {
constructor(message) { constructor(message) {
super("NeedSuiteException", Constants.res.needsuite.code, message ? message : Constants.res.needsuite.message); super('NeedSuiteException', Constants.res.needsuite.code, message ? message : Constants.res.needsuite.message);
} }
} }
+8 -10
View File
@@ -1,10 +1,8 @@
export * from "./base-controller.js"; export * from './base-controller.js';
export * from "./constants.js"; export * from './constants.js';
export * from "./crud-controller.js"; export * from './crud-controller.js';
export * from "./enum-item.js"; export * from './enum-item.js';
export * from "./exception/index.js"; export * from './exception/index.js';
export * from "./result.js"; export * from './result.js';
export * from "./base-service.js"; export * from './base-service.js';
export * from "./audit.js"; export * from "./mode.js"
export * from "./mode.js";
export * from "./core/index.js";
+4 -4
View File
@@ -1,12 +1,12 @@
let adminMode = "saas"; let adminMode = "saas"
export function setAdminMode(mode:string = "saas"){ export function setAdminMode(mode:string = "saas"){
adminMode = mode; adminMode = mode
} }
export function getAdminMode(){ export function getAdminMode(){
return adminMode; return adminMode
} }
export function isEnterprise(){ export function isEnterprise(){
return adminMode === "enterprise"; return adminMode === "enterprise"
} }
@@ -1,11 +1,11 @@
import type { IMidwayContainer } from "@midwayjs/core"; import type { IMidwayContainer } from '@midwayjs/core';
import { Configuration } from "@midwayjs/core"; import { Configuration } from '@midwayjs/core';
import { logger } from "@certd/basic"; import { logger } from '@certd/basic';
@Configuration({ @Configuration({
namespace: "lib-server", namespace: 'lib-server',
}) })
export class LibServerConfiguration { export class LibServerConfiguration {
async onReady(container: IMidwayContainer) { async onReady(container: IMidwayContainer) {
logger.info("lib start..."); logger.info('lib start...');
} }
} }
+6 -6
View File
@@ -1,9 +1,9 @@
import { SysSettingsEntity } from "./system/index.js"; import { SysSettingsEntity } from './system/index.js';
import { AccessEntity } from "./user/access/entity/access.js"; import { AccessEntity } from './user/access/entity/access.js';
import { AddonEntity } from "./user/index.js"; import { AddonEntity } from "./user/index.js";
export * from "./basic/index.js"; export * from './basic/index.js';
export * from "./system/index.js"; export * from './system/index.js';
export * from "./user/index.js"; export * from './user/index.js';
export { LibServerConfiguration as Configuration } from "./configuration.js"; export { LibServerConfiguration as Configuration } from './configuration.js';
export const libServerEntities = [SysSettingsEntity, AccessEntity,AddonEntity]; export const libServerEntities = [SysSettingsEntity, AccessEntity,AddonEntity];
@@ -1,5 +1,5 @@
export * from "./service/plus-service.js"; export * from './service/plus-service.js';
export * from "./service/file-service.js"; export * from './service/file-service.js';
export * from "./service/encryptor.js"; export * from './service/encryptor.js';
export * from "./service/ocr-service.js"; export * from './service/ocr-service.js';
export * from "./service/executor-queue.js"; export * from './service/executor-queue.js';
@@ -1,8 +1,8 @@
import crypto from "crypto"; import crypto from 'crypto';
export class Encryptor { export class Encryptor {
secretKey: Buffer; secretKey: Buffer;
constructor(encryptSecret: string, encoding: BufferEncoding = "base64") { constructor(encryptSecret: string, encoding: BufferEncoding = 'base64') {
this.secretKey = Buffer.from(encryptSecret, encoding); this.secretKey = Buffer.from(encryptSecret, encoding);
} }
// 加密函数 // 加密函数
@@ -10,18 +10,18 @@ export class Encryptor {
const iv = crypto.randomBytes(16); // 初始化向量 const iv = crypto.randomBytes(16); // 初始化向量
// const secretKey = crypto.randomBytes(32); // const secretKey = crypto.randomBytes(32);
// const key = Buffer.from(secretKey); // const key = Buffer.from(secretKey);
const cipher = crypto.createCipheriv("aes-256-cbc", this.secretKey, iv); const cipher = crypto.createCipheriv('aes-256-cbc', this.secretKey, iv);
let encrypted = cipher.update(text); let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]); encrypted = Buffer.concat([encrypted, cipher.final()]);
return iv.toString("hex") + ":" + encrypted.toString("hex"); return iv.toString('hex') + ':' + encrypted.toString('hex');
} }
// 解密函数 // 解密函数
decrypt(encryptedText: string) { decrypt(encryptedText: string) {
const textParts = encryptedText.split(":"); const textParts = encryptedText.split(':');
const iv = Buffer.from(textParts.shift(), "hex"); const iv = Buffer.from(textParts.shift(), 'hex');
const encrypted = Buffer.from(textParts.join(":"), "hex"); const encrypted = Buffer.from(textParts.join(':'), 'hex');
const decipher = crypto.createDecipheriv("aes-256-cbc", Buffer.from(this.secretKey), iv); const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this.secretKey), iv);
let decrypted = decipher.update(encrypted); let decrypted = decipher.update(encrypted);
decrypted = Buffer.concat([decrypted, decipher.final()]); decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString(); return decrypted.toString();
@@ -2,7 +2,7 @@ import { logger } from "@certd/basic";
export type TaskItem = { export type TaskItem = {
task: ()=>Promise<void>; task: ()=>Promise<void>;
}; }
export class UserTaskQueue{ export class UserTaskQueue{
userId: number; userId: number;
@@ -10,7 +10,7 @@ export class UserTaskQueue {
runningQueue: TaskItem[] = []; runningQueue: TaskItem[] = [];
getMaxRunningCount: ()=>number ; getMaxRunningCount: ()=>number ;
constructor(req: { userId: number; getMaxRunningCount: () => number }) { constructor(req: { userId: number ,getMaxRunningCount: ()=>number }) {
this.userId = req.userId; this.userId = req.userId;
this.getMaxRunningCount = req.getMaxRunningCount ; this.getMaxRunningCount = req.getMaxRunningCount ;
} }
@@ -46,9 +46,9 @@ export class UserTaskQueue {
// 继续执行下一个任务 // 继续执行下一个任务
this.runTask(); this.runTask();
} }
}; }
logger.info(`[user_${this.userId}]执行任务,当前运行队列:${this.runningQueue.length}, 等待队列:${this.pendingQueue.length}`); logger.info(`[user_${this.userId}]执行任务,当前运行队列:${this.runningQueue.length}, 等待队列:${this.pendingQueue.length}`);
call(); call()
} }
} }
@@ -56,6 +56,7 @@ export class ExecutorQueue {
queues: Record<number, UserTaskQueue> = {}; queues: Record<number, UserTaskQueue> = {};
maxRunningCount: number = 10; maxRunningCount: number = 10;
setMaxRunningCount(count: number) { setMaxRunningCount(count: number) {
this.maxRunningCount = count; this.maxRunningCount = count;
} }
@@ -72,6 +73,7 @@ export class ExecutorQueue {
const userQueue = this.getUserQueue(userId); const userQueue = this.getUserQueue(userId);
userQueue.addTask(task); userQueue.addTask(task);
} }
} }
export const executorQueue = new ExecutorQueue(); export const executorQueue = new ExecutorQueue();
@@ -1,42 +1,42 @@
import { Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import dayjs from "dayjs"; import dayjs from 'dayjs';
import path from "path"; import path from 'path';
import fs from "fs"; import fs from 'fs';
import { cache, logger, utils } from "@certd/basic"; import { cache, logger, utils } from '@certd/basic';
import { NotFoundException, ParamException, PermissionException } from "../../../basic/index.js"; import { NotFoundException, ParamException, PermissionException } from '../../../basic/index.js';
export type UploadFileItem = { export type UploadFileItem = {
filename: string; filename: string;
tmpFilePath: string; tmpFilePath: string;
}; };
const uploadRootDir = "./data/upload"; const uploadRootDir = './data/upload';
export const uploadTmpFileCacheKey = "tmpfile_key_"; export const uploadTmpFileCacheKey = 'tmpfile_key_';
/** /**
*/ */
@Provide() @Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true }) @Scope(ScopeEnum.Request, { allowDowngrade: true })
export class FileService { export class FileService {
async saveFile(userId: number, tmpCacheKey: any, permission: "public" | "private") { async saveFile(userId: number, tmpCacheKey: any, permission: 'public' | 'private') {
if (tmpCacheKey.startsWith(`/${permission}`)) { if (tmpCacheKey.startsWith(`/${permission}`)) {
//已经保存过,不需要再次保存 //已经保存过,不需要再次保存
return tmpCacheKey; return tmpCacheKey;
} }
let fileName = ""; let fileName = '';
let tmpFilePath = tmpCacheKey; let tmpFilePath = tmpCacheKey;
if (uploadTmpFileCacheKey && tmpCacheKey.startsWith(uploadTmpFileCacheKey)) { if (uploadTmpFileCacheKey && tmpCacheKey.startsWith(uploadTmpFileCacheKey)) {
const tmpFile: UploadFileItem = cache.get(tmpCacheKey); const tmpFile: UploadFileItem = cache.get(tmpCacheKey);
if (!tmpFile) { if (!tmpFile) {
throw new ParamException("文件已过期,请重新上传"); throw new ParamException('文件已过期,请重新上传');
} }
tmpFilePath = tmpFile.tmpFilePath; tmpFilePath = tmpFile.tmpFilePath;
fileName = tmpFile.filename || path.basename(tmpFilePath); fileName = tmpFile.filename || path.basename(tmpFilePath);
} }
if (!tmpFilePath || !fs.existsSync(tmpFilePath)) { if (!tmpFilePath || !fs.existsSync(tmpFilePath)) {
throw new Error("文件不存在,请重新上传"); throw new Error('文件不存在,请重新上传');
} }
const date = dayjs().format("YYYY_MM_DD"); const date = dayjs().format('YYYY_MM_DD');
const random = Math.random().toString(36).substring(7); const random = Math.random().toString(36).substring(7);
const userIdMd5 = Buffer.from(Buffer.from(userId + "").toString("base64")).toString("hex"); const userIdMd5 = Buffer.from(Buffer.from(userId + '').toString('base64')).toString('hex');
const key = `/${permission}/${userIdMd5}/${date}/${random}_${fileName}`; const key = `/${permission}/${userIdMd5}/${date}/${random}_${fileName}`;
let savePath = path.join(uploadRootDir, key); let savePath = path.join(uploadRootDir, key);
savePath = path.resolve(savePath); savePath = path.resolve(savePath);
@@ -44,6 +44,7 @@ export class FileService {
if (!fs.existsSync(parentDir)) { if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true }); fs.mkdirSync(parentDir, { recursive: true });
} }
// eslint-disable-next-line node/no-unsupported-features/node-builtins
const copyFile = utils.promises.promisify(fs.copyFile); const copyFile = utils.promises.promisify(fs.copyFile);
await copyFile(tmpFilePath, savePath); await copyFile(tmpFilePath, savePath);
try { try {
@@ -57,29 +58,29 @@ export class FileService {
getFile(key: string, userId?: number, allowAnyPrivateUser = false) { getFile(key: string, userId?: number, allowAnyPrivateUser = false) {
if (!key) { if (!key) {
throw new ParamException("参数错误"); throw new ParamException('参数错误');
} }
if (key.indexOf("..") >= 0) { if (key.indexOf('..') >= 0) {
//安全性判断 //安全性判断
throw new ParamException("参数错误"); throw new ParamException('参数错误');
} }
if (!key.startsWith("/")) { if (!key.startsWith('/')) {
throw new ParamException("参数错误"); throw new ParamException('参数错误');
} }
const keyArr = key.split("/"); const keyArr = key.split('/');
const permission = keyArr[1]; const permission = keyArr[1];
const userIdMd5 = keyArr[2]; const userIdMd5 = keyArr[2];
if (permission !== "public" && !allowAnyPrivateUser) { if (permission !== 'public' && !allowAnyPrivateUser) {
//非公开文件需要验证用户 //非公开文件需要验证用户
const userIdStr = Buffer.from(Buffer.from(userIdMd5, "hex").toString("base64")).toString(); const userIdStr = Buffer.from(Buffer.from(userIdMd5, 'hex').toString('base64')).toString();
const userIdInt: number = parseInt(userIdStr, 10); const userIdInt: number = parseInt(userIdStr, 10);
if (userId == null || userIdInt !== userId) { if (userId == null || userIdInt !== userId) {
throw new PermissionException("无访问权限"); throw new PermissionException('无访问权限');
} }
} }
const filePath = path.join(uploadRootDir, key); const filePath = path.join(uploadRootDir, key);
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
throw new NotFoundException("文件不存在"); throw new NotFoundException('文件不存在');
} }
return filePath; return filePath;
} }
@@ -15,9 +15,10 @@ export class OcrService implements IOcrService {
url: "/activation/certd/ocr", url: "/activation/certd/ocr",
method: "post", method: "post",
data: { data: {
image: opts.image, image: opts.image
}, }
}); });
return res; return res;
} }
} }
@@ -164,7 +164,7 @@ export class PlusService {
baseURL: plusRequestService.getBaseURL(), baseURL: plusRequestService.getBaseURL(),
method: "post", method: "post",
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Berear ${token}`,
}, },
}); });
const res = await http.request(config); const res = await http.request(config);
@@ -173,9 +173,4 @@ export class PlusService {
} }
return res.data; return res.data;
} }
async request(config: HttpRequestConfig) {
const plusRequestService = await this.getPlusRequestService();
return await plusRequestService.request(config);
}
} }
+2 -2
View File
@@ -1,2 +1,2 @@
export * from "./settings/index.js"; export * from './settings/index.js';
export * from "./basic/index.js"; export * from './basic/index.js';
@@ -1,3 +1,3 @@
export * from "./service/sys-settings-service.js"; export * from './service/sys-settings-service.js';
export * from "./service/models.js"; export * from './service/models.js';
export * from "./entity/sys-settings.js"; export * from './entity/sys-settings.js';
@@ -279,11 +279,3 @@ export class SysSafeSetting extends BaseSettings {
autoHiddenTimes: 5, autoHiddenTimes: 5,
}; };
} }
export class SysPluginSetting extends BaseSettings {
static __title__ = "系统插件设置";
static __key__ = "sys.plugin";
static __access__ = "private";
lastSyncTime?: number;
}
@@ -1,15 +1,15 @@
import { Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from "@midwayjs/typeorm"; import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from "typeorm"; import { Repository } from 'typeorm';
import { SysSettingsEntity } from "../entity/sys-settings.js"; import { SysSettingsEntity } from '../entity/sys-settings.js';
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from "./models.js"; import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from './models.js';
import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from "@certd/acme-client"; import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from '@certd/acme-client';
import { cache, logger, mergeUtils, setGlobalHeaders, setGlobalProxy } from "@certd/basic"; import { cache, logger, mergeUtils, setGlobalHeaders, setGlobalProxy } from '@certd/basic';
import { isPlus } from "@certd/plus-core"; import { isPlus } from '@certd/plus-core';
import * as dns from "node:dns"; import * as dns from 'node:dns';
import { BaseService, setAdminMode } from "../../../basic/index.js"; import { BaseService, setAdminMode } from '../../../basic/index.js';
import { executorQueue } from "../../basic/service/executor-queue.js"; import { executorQueue } from '../../basic/service/executor-queue.js';
const { merge } = mergeUtils; const { merge } = mergeUtils;
let lastSaveEnvVars = {}; let lastSaveEnvVars = {};
@@ -138,7 +138,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
res.reverseProxies[domain] = ""; res.reverseProxies[domain] = "";
} }
} }
return res; return res
} }
async savePrivateSettings(bean: SysPrivateSettings) { async savePrivateSettings(bean: SysPrivateSettings) {
@@ -149,14 +149,14 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
} }
async reloadSettings() { async reloadSettings() {
await this.reloadPrivateSettings(); await this.reloadPrivateSettings()
await this.reloadPublicSettings(); await this.reloadPublicSettings()
} }
async reloadPublicSettings() { async reloadPublicSettings() {
const publicSetting = await this.getPublicSettings(); const publicSetting = await this.getPublicSettings()
if (isPlus()){ if (isPlus()){
setAdminMode(publicSetting.adminMode); setAdminMode(publicSetting.adminMode )
} }
} }
@@ -183,28 +183,29 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
this.setEnvironmentVars(privateSetting.environmentVars); this.setEnvironmentVars(privateSetting.environmentVars);
setWalkFromAuthoritative(privateSetting.acmeWalkFromAuthoritative); setWalkFromAuthoritative(privateSetting.acmeWalkFromAuthoritative);
} }
parseKeyValueText(text: string) { parseKeyValueText(text: string) {
const values = {}; const values = {};
if (typeof text !== "string") { if (typeof text !== 'string') {
text = ""; text = "";
} }
text.split("\n").forEach(line => { text.split('\n').forEach(line => {
line = line.trim(); line = line.trim();
if (!line || line.startsWith("#")) { if (!line || line.startsWith('#')) {
return; return
} }
const arr = line.split("#"); const arr = line.split("#")
if (arr.length > 0) { if (arr.length > 0) {
line = arr[0].trim(); line = arr[0].trim();
} }
if (!line.includes("=")) { if (!line.includes("=")) {
return; return
} }
const eqIndex = line.indexOf("="); const eqIndex = line.indexOf('=');
const key = line.substring(0, eqIndex).trim(); const key = line.substring(0, eqIndex).trim();
const value = line.substring(eqIndex + 1).trim(); const value = line.substring(eqIndex + 1).trim();
if (key && value) { if (key && value) {
@@ -233,7 +234,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
entity.setting = JSON.stringify(setting); entity.setting = JSON.stringify(setting);
await this.repository.save(entity); await this.repository.save(entity);
} else { } else {
throw new Error("该设置不存在"); throw new Error('该设置不存在');
} }
cache.delete(`settings.${key}`); cache.delete(`settings.${key}`);
} }
@@ -245,20 +246,20 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
if (settings == null) { if (settings == null) {
const backup = new SysSecretBackup(); const backup = new SysSecretBackup();
if (installInfo.siteId == null || privateSettings.encryptSecret == null) { if (installInfo.siteId == null || privateSettings.encryptSecret == null) {
logger.error("备份密钥失败,siteId或encryptSecret为空"); logger.error('备份密钥失败,siteId或encryptSecret为空');
return; return;
} }
backup.siteId = installInfo.siteId; backup.siteId = installInfo.siteId;
backup.encryptSecret = privateSettings.encryptSecret; backup.encryptSecret = privateSettings.encryptSecret;
await this.saveSetting(backup); await this.saveSetting(backup);
logger.info("备份密钥成功"); logger.info('备份密钥成功');
} else { } else {
//校验是否有变化 //校验是否有变化
if (settings.siteId !== installInfo.siteId) { if (settings.siteId !== installInfo.siteId) {
throw new Error(`siteId与备份不一致,可能是数据异常,请检查:backup=${settings.siteId}, current=${installInfo.siteId}`); throw new Error(`siteId与备份不一致,可能是数据异常,请检查:backup=${settings.siteId}, current=${installInfo.siteId}`);
} }
if (settings.encryptSecret !== privateSettings.encryptSecret) { if (settings.encryptSecret !== privateSettings.encryptSecret) {
throw new Error("encryptSecret与备份不一致,可能是数据异常,请检查"); throw new Error('encryptSecret与备份不一致,可能是数据异常,请检查');
} }
} }
} }
@@ -270,12 +271,12 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
//从备份中读取 //从备份中读取
const settings = await this.getSettingByKey(SysSecretBackup.__key__); const settings = await this.getSettingByKey(SysSecretBackup.__key__);
if (settings == null || !settings.encryptSecret) { if (settings == null || !settings.encryptSecret) {
throw new Error("密钥备份不存在"); throw new Error('密钥备份不存在');
} }
sysSecret.siteId = settings.siteId; sysSecret.siteId = settings.siteId;
sysSecret.encryptSecret = settings.encryptSecret; sysSecret.encryptSecret = settings.encryptSecret;
await this.saveSetting(sysSecret); await this.saveSetting(sysSecret);
logger.info("密钥恢复成功"); logger.info('密钥恢复成功');
return sysSecret; return sysSecret;
} }
} }
@@ -1,46 +1,46 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
/** /**
* *
*/ */
@Entity("cd_access") @Entity('cd_access')
export class AccessEntity { export class AccessEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@Column({ name: "key_id", comment: "key_id", length: 100 }) @Column({ name: 'key_id', comment: 'key_id', length: 100 })
keyId: string; keyId: string;
@Column({ name: "user_id", comment: "用户id" }) @Column({ name: 'user_id', comment: '用户id' })
userId: number; // 0为系统级别, -1为企业,大于1为用户 userId: number; // 0为系统级别, -1为企业,大于1为用户
@Column({ comment: "名称", length: 100 }) @Column({ comment: '名称', length: 100 })
name: string; name: string;
@Column({ comment: "类型", length: 100 }) @Column({ comment: '类型', length: 100 })
type: string; type: string;
@Column({ name: "subtype", comment: "子类型", length: 100, nullable: true }) @Column({ name: 'subtype', comment: '子类型', length: 100, nullable: true })
subtype: string; subtype: string;
@Column({ name: "setting", comment: "设置", length: 10240, nullable: true }) @Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
setting: string; setting: string;
@Column({ name: "encrypt_setting", comment: "已加密设置", length: 10240, nullable: true }) @Column({ name: 'encrypt_setting', comment: '已加密设置', length: 10240, nullable: true })
encryptSetting: string; encryptSetting: string;
@Column({ name: "project_id", comment: "项目id" }) @Column({ name: 'project_id', comment: '项目id' })
projectId: number; projectId: number;
@Column({ @Column({
name: "create_time", name: 'create_time',
comment: "创建时间", comment: '创建时间',
default: () => "CURRENT_TIMESTAMP", default: () => 'CURRENT_TIMESTAMP',
}) })
createTime: Date; createTime: Date;
@Column({ @Column({
name: "update_time", name: 'update_time',
comment: "修改时间", comment: '修改时间',
default: () => "CURRENT_TIMESTAMP", default: () => 'CURRENT_TIMESTAMP',
}) })
updateTime: Date; updateTime: Date;
} }
@@ -1,5 +1,5 @@
export * from "./entity/access.js"; export * from './entity/access.js';
export * from "./service/access-service.js"; export * from './service/access-service.js';
export * from "./service/access-sys-getter.js"; export * from './service/access-sys-getter.js';
export * from "./service/access-getter.js"; export * from './service/access-getter.js';
export * from "./service/encrypt-service.js"; export * from './service/encrypt-service.js';
@@ -1,5 +1,5 @@
import { IAccessService } from "@certd/pipeline"; import { IAccessService } from '@certd/pipeline';
import { AccessService } from "./access-service.js"; import { AccessService } from './access-service.js';
export class AccessSysGetter implements IAccessService { export class AccessSysGetter implements IAccessService {
accessService: AccessService; accessService: AccessService;
@@ -1,5 +1,5 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { Encryptor, SysSecret, SysSettingsService } from "../../../system/index.js"; import { Encryptor, SysSecret, SysSettingsService } from '../../../system/index.js';
/** /**
* *
@@ -49,7 +49,7 @@ export function AddonInput(input?: AddonInputDefine): PropertyDecorator {
} }
export async function newAddon(addonType:string,type: string, input: any, ctx: AddonContext) { export async function newAddon(addonType:string,type: string, input: any, ctx: AddonContext) {
const key = `${addonType}:${type}`; const key = `${addonType}:${type}`
const register = addonRegistry.get(key); const register = addonRegistry.get(key);
if (register == null) { if (register == null) {
throw new Error(`${addonType} ${type} not found`); throw new Error(`${addonType} ${type} not found`);
@@ -1,46 +1,49 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
/** /**
*/ */
@Entity("cd_addon") @Entity('cd_addon')
export class AddonEntity { export class AddonEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@Column({ name: "key_id", comment: "key_id", length: 100 }) @Column({ name: 'key_id', comment: 'key_id', length: 100 })
keyId: string; keyId: string;
@Column({ name: "user_id", comment: "用户id" }) @Column({ name: 'user_id', comment: '用户id' })
userId: number; userId: number;
@Column({ comment: "名称", length: 100 }) @Column({ comment: '名称', length: 100 })
name: string; name: string;
@Column({ name: "addon_type", comment: "addon类型", length: 100 })
@Column({ name: 'addon_type', comment: 'addon类型', length: 100 })
addonType: string; addonType: string;
@Column({ comment: "类型", length: 100 })
@Column({ comment: '类型', length: 100 })
type: string; type: string;
@Column({ name: "setting", comment: "设置", length: 10240, nullable: true }) @Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
setting: string; setting: string;
@Column({ name: "is_system", comment: "是否系统级别", nullable: false, default: false }) @Column({ name: 'is_system', comment: '是否系统级别', nullable: false, default: false })
isSystem: boolean; isSystem: boolean;
@Column({ name: "is_default", comment: "是否默认", nullable: false, default: false }) @Column({ name: 'is_default', comment: '是否默认', nullable: false, default: false })
isDefault: boolean; isDefault: boolean;
@Column({ name: "project_id", comment: "项目id" }) @Column({ name: 'project_id', comment: '项目id' })
projectId: number; projectId: number;
@Column({ @Column({
name: "create_time", name: 'create_time',
comment: "创建时间", comment: '创建时间',
default: () => "CURRENT_TIMESTAMP", default: () => 'CURRENT_TIMESTAMP',
}) })
createTime: Date; createTime: Date;
@Column({ @Column({
name: "update_time", name: 'update_time',
comment: "修改时间", comment: '修改时间',
default: () => "CURRENT_TIMESTAMP", default: () => 'CURRENT_TIMESTAMP',
}) })
updateTime: Date; updateTime: Date;
} }
@@ -1,3 +1,3 @@
export * from "./api/index.js"; export * from './api/index.js'
export * from "./entity/addon.js"; export * from './entity/addon.js'
export * from "./service/addon-service.js"; export * from './service/addon-service.js'
@@ -49,6 +49,7 @@ export class AddonService extends BaseService<AddonEntity> {
return await super.add(param); return await super.add(param);
} }
/** /**
* *
* @param param * @param param
@@ -58,7 +59,7 @@ export class AddonService extends BaseService<AddonEntity> {
if (oldEntity == null) { if (oldEntity == null) {
throw new ValidateException("该Addon配置不存在,请确认是否已被删除"); throw new ValidateException("该Addon配置不存在,请确认是否已被删除");
} }
delete param.keyId; delete param.keyId
return await super.update(param); return await super.update(param);
} }
@@ -74,10 +75,11 @@ export class AddonService extends BaseService<AddonEntity> {
userId: entity.userId, userId: entity.userId,
addonType: entity.addonType, addonType: entity.addonType,
type: entity.type, type: entity.type,
projectId: entity.projectId, projectId: entity.projectId
}; };
} }
getDefineList(addonType: string) { getDefineList(addonType: string) {
return addonRegistry.getDefineList(addonType); return addonRegistry.getDefineList(addonType);
} }
@@ -86,6 +88,7 @@ export class AddonService extends BaseService<AddonEntity> {
return addonRegistry.getDefine(type, prefix) as AddonDefine; return addonRegistry.getDefine(type, prefix) as AddonDefine;
} }
async getSimpleByIds(ids: number[], userId: any,projectId?:number) { async getSimpleByIds(ids: number[], userId: any,projectId?:number) {
if (ids.length === 0) { if (ids.length === 0) {
return []; return [];
@@ -106,10 +109,12 @@ export class AddonService extends BaseService<AddonEntity> {
addonType: true, addonType: true,
type: true, type: true,
userId: true, userId: true,
isSystem: true, isSystem: true
},
});
} }
});
}
async getDefault(userId: number, addonType: string,projectId?:number): Promise<any> { async getDefault(userId: number, addonType: string,projectId?:number): Promise<any> {
const userProjectQuery = this.buildUserProjectQuery(userId, projectId); const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
@@ -119,8 +124,8 @@ export class AddonService extends BaseService<AddonEntity> {
...userProjectQuery, ...userProjectQuery,
}, },
order: { order: {
isDefault: "DESC", isDefault: "DESC"
}, }
}); });
if (!res) { if (!res) {
return null; return null;
@@ -138,7 +143,7 @@ export class AddonService extends BaseService<AddonEntity> {
name: res.name, name: res.name,
userId: res.userId, userId: res.userId,
setting, setting,
projectId: res.projectId, projectId: res.projectId
}; };
} }
@@ -155,17 +160,14 @@ export class AddonService extends BaseService<AddonEntity> {
...userProjectQuery, ...userProjectQuery,
}; };
await this.repository.update(query, { await this.repository.update(query, {
isDefault: false, isDefault: false
});
await this.repository.update({ ...query, id }, {
isDefault: true
}); });
await this.repository.update(
{ ...query, id },
{
isDefault: true,
}
);
} }
async getOrCreateDefault(opts: { addonType: string; type: string; inputs: any; userId: any; projectId?: number }) { async getOrCreateDefault(opts: { addonType: string, type: string, inputs: any, userId: any,projectId?:number }) {
const { addonType, type, inputs, userId,projectId } = opts; const { addonType, type, inputs, userId,projectId } = opts;
const addonDefine = this.getDefineByType(type, addonType); const addonDefine = this.getDefineByType(type, addonType);
@@ -175,7 +177,7 @@ export class AddonService extends BaseService<AddonEntity> {
return defaultConfig; return defaultConfig;
} }
const setting = { const setting = {
...inputs, ...inputs
}; };
const res = await this.repository.save({ const res = await this.repository.save({
userId, userId,
@@ -184,19 +186,19 @@ export class AddonService extends BaseService<AddonEntity> {
name: addonDefine.title, name: addonDefine.title,
setting: JSON.stringify(setting), setting: JSON.stringify(setting),
isDefault: true, isDefault: true,
projectId, projectId
}); });
return this.buildAddonInstanceConfig(res); return this.buildAddonInstanceConfig(res);
} }
async getOneByType(req: { addonType: string; type: string; userId: number; projectId?: number }) { async getOneByType(req:{addonType:string,type:string,userId:number,projectId?:number}) {
const userProjectQuery = this.buildUserProjectQuery(req.userId, req.projectId); const userProjectQuery = this.buildUserProjectQuery(req.userId, req.projectId);
return await this.repository.findOne({ return await this.repository.findOne({
where: { where: {
addonType: req.addonType, addonType: req.addonType,
type: req.type, type: req.type,
...userProjectQuery, ...userProjectQuery,
}, }
}); });
} }
} }
+2 -2
View File
@@ -1,2 +1,2 @@
export * from "./access/index.js"; export * from './access/index.js';
export * from "./addon/index.js"; export * from './addon/index.js';
+1 -1
View File
@@ -19,7 +19,7 @@
"pub": "npm publish", "pub": "npm publish",
"compile": "npm run build", "compile": "npm run build",
"format": "prettier --write src", "format": "prettier --write src",
"lint": "eslint --fix --ext .ts src" "lint": "eslint --fix"
}, },
"keywords": [], "keywords": [],
"author": "greper", "author": "greper",
@@ -1,11 +1,11 @@
import { Config, Configuration, Logger } from "@midwayjs/core"; import { Config, Configuration, Logger } from '@midwayjs/core';
import { Flyway } from "./flyway.js"; import { Flyway } from './flyway.js';
import type { ILogger } from "@midwayjs/logger"; import type { ILogger } from '@midwayjs/logger';
import { TypeORMDataSourceManager } from "@midwayjs/typeorm"; import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
import type { IMidwayContainer } from "@midwayjs/core"; import type { IMidwayContainer } from '@midwayjs/core';
@Configuration({ @Configuration({
namespace: "flyway", namespace: 'flyway',
//importConfigs: [join(__dirname, './config')], //importConfigs: [join(__dirname, './config')],
}) })
export class FlywayConfiguration { export class FlywayConfiguration {
@@ -14,9 +14,9 @@ export class FlywayConfiguration {
@Logger() @Logger()
logger!: ILogger; logger!: ILogger;
async onReady(container: IMidwayContainer) { async onReady(container: IMidwayContainer) {
this.logger.info("flyway start:" + JSON.stringify(this.flyway)); this.logger.info('flyway start:' + JSON.stringify(this.flyway));
const dataSourceManager = await container.getAsync(TypeORMDataSourceManager); const dataSourceManager = await container.getAsync(TypeORMDataSourceManager);
const dataSourceName = this.flyway.dataSourceName || "default"; const dataSourceName = this.flyway.dataSourceName || 'default';
const connection = dataSourceManager.getDataSource(dataSourceName); const connection = dataSourceManager.getDataSource(dataSourceName);
await new Flyway({ ...this.flyway, logger: this.logger, connection }).run(); await new Flyway({ ...this.flyway, logger: this.logger, connection }).run();
} }
+6 -6
View File
@@ -1,23 +1,23 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity("flyway_history") @Entity('flyway_history')
export class FlywayHistory { export class FlywayHistory {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id?: number; id?: number;
@Column({ comment: "文件名", length: 100 }) @Column({ comment: '文件名', length: 100 })
name?: string; name?: string;
@Column({ comment: "hash", length: 32 }) @Column({ comment: 'hash', length: 32 })
hash?: string; hash?: string;
@Column({ @Column({
comment: "执行时间", comment: '执行时间',
}) })
timestamp?: Date; timestamp?: Date;
@Column({ @Column({
comment: "执行成功", comment: '执行成功',
default: true, default: true,
}) })
success?: boolean; success?: boolean;

Some files were not shown because too many files have changed in this diff Show More