Compare commits

..
95 changed files with 1576 additions and 1155 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"
+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",
@@ -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;
@@ -32,7 +32,7 @@ export class IframeClient {
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;
window.addEventListener("message", 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);
@@ -40,20 +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);
} }
} }
}); });
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);
@@ -106,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",
+35 -35
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,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,8 +3,8 @@
*/ */
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;
this.code = code; this.code = code;
@@ -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,12 +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;
constructor(message, leftCount: number) { constructor(message, leftCount: 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;
} }
} }
@@ -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 -8
View File
@@ -1,8 +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 "./mode.js"; export * from "./mode.js"
+7 -7
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...');
} }
} }
+7 -7
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();
@@ -1,18 +1,18 @@
import { logger } from "@certd/basic"; 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;
pendingQueue: TaskItem[] = []; pendingQueue: TaskItem[] = [];
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 ;
} }
addTask(task: TaskItem) { addTask(task: TaskItem) {
@@ -34,10 +34,10 @@ export class UserTaskQueue {
} }
// 执行任务 // 执行任务
this.runningQueue.push(task); this.runningQueue.push(task);
const call = async () => { const call = async ()=>{
try { try{
await task.task(); await task.task();
} finally { }finally{
// 任务执行完成,从运行队列中移除 // 任务执行完成,从运行队列中移除
const index = this.runningQueue.indexOf(task); const index = this.runningQueue.indexOf(task);
if (index > -1) { if (index > -1) {
@@ -46,16 +46,17 @@ 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()
} }
} }
export class ExecutorQueue { 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;
} }
@@ -63,7 +64,7 @@ export class ExecutorQueue {
getUserQueue(userId: number) { getUserQueue(userId: number) {
const userQueue = this.queues[userId]; const userQueue = this.queues[userId];
if (!userQueue) { if (!userQueue) {
this.queues[userId] = new UserTaskQueue({ userId, getMaxRunningCount: () => this.maxRunningCount }); this.queues[userId] = new UserTaskQueue({ userId, getMaxRunningCount: ()=>this.maxRunningCount });
} }
return this.queues[userId]; return this.queues[userId];
} }
@@ -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;
} }
} }
+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';
@@ -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';
/** /**
* 授权 * 授权
@@ -48,8 +48,8 @@ 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,11 +88,12 @@ 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 [];
} }
if (userId == null) { if (userId==null) {
return []; return [];
} }
const userProjectQuery = this.buildUserProjectQuery(userId, projectId); const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
@@ -106,12 +109,14 @@ 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);
const res = await this.repository.findOne({ const res = await this.repository.findOne({
where: { where: {
@@ -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,15 +143,15 @@ 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
}; };
} }
async setDefault(id: number, userId: number, addonType: string, projectId?: number) { async setDefault(id: number, userId: number, addonType: string,projectId?:number) {
if (!id) { if (!id) {
throw new ValidateException("id不能为空"); throw new ValidateException("id不能为空");
} }
if (userId == null) { if (userId==null) {
throw new ValidateException("userId不能为空"); throw new ValidateException("userId不能为空");
} }
const userProjectQuery = this.buildUserProjectQuery(userId, projectId); const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
@@ -155,27 +160,24 @@ 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);
const defaultConfig = await this.getDefault(userId, addonType, projectId); const defaultConfig = await this.getDefault(userId, addonType,projectId);
if (defaultConfig) { if (defaultConfig) {
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;
+5 -8
View File
@@ -93,21 +93,19 @@ export class Flyway {
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
} catch (err) { } catch (err) {
this.logger.error(err); this.logger.error(err);
this.errorTip(err);
await this.storeSqlExecLog(file.script, filepath, false, queryRunner); await this.storeSqlExecLog(file.script, filepath, false, queryRunner);
await queryRunner.rollbackTransaction(); await queryRunner.rollbackTransaction();
if (err.code === "SQLITE_IOERR_WRITE") {
this.logger.warn("SQLite数据库写入失败,可能您的操作系统版本太低,请将「certd:latest」镜像改为「certd:slim」即可。(如需指定版本可以修改成「certd:[version]-slim」)", file.script);
}
throw err; throw err;
} }
} }
this.logger.info("[ midfly ] end-------------"); this.logger.info("[ midfly ] end-------------");
} }
private errorTip(err: any) {
if (err.code === "SQLITE_IOERR_WRITE") {
this.logger.warn("SQLite数据库写入失败,可能您的操作系统版本太低,请将「certd:latest」镜像改为「certd:slim」即可。(如需指定版本可以修改成「certd:[version]-slim」)");
}
}
private async storeSqlExecLog(filename: string, filepath: string, success: boolean, queryRunner: QueryRunner) { private async storeSqlExecLog(filename: string, filepath: string, success: boolean, queryRunner: QueryRunner) {
const hash = await this.getFileHash(filepath); const hash = await this.getFileHash(filepath);
//先删除 //先删除
@@ -267,7 +265,6 @@ export class Flyway {
await queryRunner.query(sql); await queryRunner.query(sql);
} catch (err: any) { } catch (err: any) {
this.logger.error("exec sql error ", err.message, err); this.logger.error("exec sql error ", err.message, err);
this.errorTip(err);
throw err; throw err;
} }
} }
+3 -3
View File
@@ -1,3 +1,3 @@
export { FlywayConfiguration as Configuration } from "./configuration.js"; export { FlywayConfiguration as Configuration } from './configuration.js';
export { Flyway, setFlywayLogger } from "./flyway.js"; export { Flyway, setFlywayLogger } from './flyway.js';
export { FlywayHistory } from "./entity.js"; export { FlywayHistory } from './entity.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": {
"@certd/plugin-lib": "^1.42.6" "@certd/plugin-lib": "^1.42.6"
+1 -1
View File
@@ -14,7 +14,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/acme-client": "^1.42.6", "@certd/acme-client": "^1.42.6",
@@ -64,7 +64,6 @@ export default {
projectUserManager: "Project User Management", projectUserManager: "Project User Management",
myProjectManager: "My Projects", myProjectManager: "My Projects",
myProjectDetail: "Project Detail", myProjectDetail: "Project Detail",
projectDetail: "Project Detail",
projectJoin: "Join Project", projectJoin: "Join Project",
currentProject: "Current Project", currentProject: "Current Project",
projectMemberManager: "Project Member", projectMemberManager: "Project Member",
@@ -31,15 +31,6 @@ export default {
copyPreferences: "Copy Preferences", copyPreferences: "Copy Preferences",
copyPreferencesSuccessTitle: "Copy successful", copyPreferencesSuccessTitle: "Copy successful",
copyPreferencesSuccess: "Copy successful, please override in `src/preferences.ts` under app", copyPreferencesSuccess: "Copy successful, please override in `src/preferences.ts` under app",
importPreferences: "Import from Clipboard",
importPreferencesSuccessTitle: "Import successful",
importPreferencesSuccess: "Preferences imported from clipboard",
importPreferencesErrorTitle: "Import failed",
importPreferencesError: "Clipboard content is invalid. Please copy preferences JSON first",
saveToAccount: "Save to Account",
saveToAccountSuccess: "Preferences saved to account",
saveToAccountError: "Failed to save preferences to account",
saveToAccountNeedLogin: "Please sign in before saving to account",
clearAndLogout: "Clear Cache & Logout", clearAndLogout: "Clear Cache & Logout",
mode: "Mode", mode: "Mode",
general: "General", general: "General",
@@ -64,7 +64,6 @@ export default {
enterpriseSetting: "企业设置", enterpriseSetting: "企业设置",
myProjectManager: "我的项目", myProjectManager: "我的项目",
myProjectDetail: "项目详情", myProjectDetail: "项目详情",
projectDetail: "项目详情",
projectJoin: "加入项目", projectJoin: "加入项目",
currentProject: "当前项目", currentProject: "当前项目",
projectMemberManager: "项目成员管理", projectMemberManager: "项目成员管理",
@@ -31,15 +31,6 @@ export default {
copyPreferences: "复制偏好设置", copyPreferences: "复制偏好设置",
copyPreferencesSuccessTitle: "复制成功", copyPreferencesSuccessTitle: "复制成功",
copyPreferencesSuccess: "复制成功,请在 app 下的 `src/preferences.ts`内进行覆盖", copyPreferencesSuccess: "复制成功,请在 app 下的 `src/preferences.ts`内进行覆盖",
importPreferences: "从剪切板导入",
importPreferencesSuccessTitle: "导入成功",
importPreferencesSuccess: "已从剪切板导入偏好设置",
importPreferencesErrorTitle: "导入失败",
importPreferencesError: "剪切板内容无效,请先复制偏好设置 JSON",
saveToAccount: "保存到账号",
saveToAccountSuccess: "偏好设置已保存到账号",
saveToAccountError: "保存到账号失败",
saveToAccountNeedLogin: "请先登录后再保存到账号",
clearAndLogout: "清空缓存 & 退出登录", clearAndLogout: "清空缓存 & 退出登录",
mode: "模式", mode: "模式",
general: "通用", general: "通用",
@@ -390,10 +390,4 @@ export const useSettingStore = defineStore({
mitter.on("app.login", async () => { mitter.on("app.login", async () => {
await useSettingStore().init(); await useSettingStore().init();
try {
const { loadPreferencesFromAccount } = await import("/@/vben/layouts/widgets/preferences/account-sync");
await loadPreferencesFromAccount();
} catch (e) {
console.error("加载账号偏好设置失败", e);
}
}); });
@@ -18,8 +18,6 @@ export {
CircleCheckBig, CircleCheckBig,
CircleHelp, CircleHelp,
Copy, Copy,
ClipboardPaste,
CloudUpload,
CornerDownLeft, CornerDownLeft,
Ellipsis, Ellipsis,
Expand, Expand,
@@ -1,66 +0,0 @@
import { message as antdMessage } from "ant-design-vue";
import { $t, loadLocaleMessages } from "/@/locales";
import { updatePreferences } from "/@/vben/preferences";
import { PreferencesSettingsGet, PreferencesSettingsSave } from "./api";
const PREFERENCES_KNOWN_KEYS = ["app", "theme", "logo", "sidebar", "header", "tabbar", "breadcrumb", "navigation", "widget", "footer", "copyright", "shortcutKeys", "transition"];
export function isPreferencesPayload(value: unknown): value is Record<string, any> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
}
return PREFERENCES_KNOWN_KEYS.some(key => Object.prototype.hasOwnProperty.call(value, key));
}
/** 保存到账号:允许空对象(重置后的默认偏好) */
export function isPreferencesSavePayload(value: unknown): value is Record<string, any> {
if (value == null || typeof value !== "object" || Array.isArray(value)) {
return false;
}
const keys = Object.keys(value as Record<string, any>);
if (keys.length === 0) {
return true;
}
return isPreferencesPayload(value);
}
export async function applyPreferencesFromAccount(data: Record<string, any>) {
updatePreferences(data);
if (data.app?.locale) {
await loadLocaleMessages(data.app.locale);
}
}
/**
* 登录后从账号加载偏好并应用到本地;
* - null / {}:账号无有效配置(含保存为空),默认继续使用本地偏好
*/
export async function loadPreferencesFromAccount() {
const data = await PreferencesSettingsGet();
if (data == null) {
return false;
}
if (typeof data !== "object" || Array.isArray(data)) {
return false;
}
// 保存为空时,后续拉取到空数据后默认从本地读取,不覆盖本地
if (Object.keys(data).length === 0) {
return false;
}
if (!isPreferencesPayload(data)) {
return false;
}
await applyPreferencesFromAccount(data);
return true;
}
export async function savePreferencesToAccount(preferencesPayload: Record<string, any> | null | undefined) {
const payload = preferencesPayload && typeof preferencesPayload === "object" ? preferencesPayload : {};
if (!isPreferencesSavePayload(payload)) {
throw new Error("invalid preferences payload");
}
await PreferencesSettingsSave(payload);
antdMessage.success($t("preferences.saveToAccountSuccess"));
}
@@ -1,21 +0,0 @@
// @ts-ignore
import { request } from "/@/api/service";
const apiPrefix = "/user/settings/preferences";
export async function PreferencesSettingsGet(): Promise<Record<string, any> | null> {
const res = await request({
url: apiPrefix + "/get",
method: "post",
showErrorNotify: false,
});
return (res as Record<string, any>) || null;
}
export async function PreferencesSettingsSave(preferences: Record<string, any>) {
return await request({
url: apiPrefix + "/save",
method: "post",
data: { preferences },
});
}
@@ -16,27 +16,21 @@ import type { SegmentedItem } from "/@/vben//shadcn-ui";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { ClipboardPaste, CloudUpload, Copy, RotateCw, X } from "/@/vben/icons"; import { Copy, RotateCw, X } from "/@/vben/icons";
import { $t, loadLocaleMessages } from "/@/locales"; import { $t, loadLocaleMessages } from "/@/locales";
import { clearPreferencesCache, preferences, resetPreferences, usePreferences } from "/@/vben/preferences"; import { clearPreferencesCache, preferences, resetPreferences, usePreferences } from "/@/vben/preferences";
import { useVbenDrawer } from "/@/vben//popup-ui"; import { useVbenDrawer } from "/@/vben//popup-ui";
import { VbenButton, VbenIconButton, VbenSegmented } from "/@/vben//shadcn-ui"; import { VbenButton, VbenIconButton, VbenSegmented } from "/@/vben//shadcn-ui";
import { globalShareState } from "/@/vben//shared/global-state"; import { globalShareState } from "/@/vben//shared/global-state";
import { useUserStore } from "/@/store/user";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { Animation, Block, Breadcrumb, BuiltinTheme, ColorMode, Content, Copyright, Footer, General, GlobalShortcutKeys, Header, Layout, Navigation, Radius, Sidebar, Tabbar, Theme, Widget } from "./blocks"; import { Animation, Block, Breadcrumb, BuiltinTheme, ColorMode, Content, Copyright, Footer, General, GlobalShortcutKeys, Header, Layout, Navigation, Radius, Sidebar, Tabbar, Theme, Widget } from "./blocks";
import { applyPreferencesFromAccount, isPreferencesPayload, savePreferencesToAccount } from "./account-sync";
import { message as antdMessage } from "ant-design-vue";
const emit = defineEmits<{ clearPreferencesAndLogout: [] }>(); const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
const message = globalShareState.getMessage(); const message = globalShareState.getMessage();
const userStore = useUserStore();
const savingToAccount = ref(false);
const appLocale = defineModel<SupportedLanguagesType>("appLocale"); const appLocale = defineModel<SupportedLanguagesType>("appLocale");
const appDynamicTitle = defineModel<boolean>("appDynamicTitle"); const appDynamicTitle = defineModel<boolean>("appDynamicTitle");
@@ -156,41 +150,6 @@ async function handleCopy() {
await copy(JSON.stringify(diffPreference.value, null, 2)); await copy(JSON.stringify(diffPreference.value, null, 2));
message.copyPreferencesSuccess?.($t("preferences.copyPreferencesSuccessTitle"), $t("preferences.copyPreferencesSuccess")); message.copyPreferencesSuccess?.($t("preferences.copyPreferencesSuccessTitle"), $t("preferences.copyPreferencesSuccess"));
antdMessage.success($t("preferences.copyPreferencesSuccessTitle"));
}
async function handleImport() {
try {
const text = await navigator.clipboard.readText();
const data = JSON.parse(text);
if (!isPreferencesPayload(data)) {
throw new Error("invalid preferences payload");
}
await applyPreferencesFromAccount(data);
message.copyPreferencesSuccess?.($t("preferences.importPreferencesSuccessTitle"), $t("preferences.importPreferencesSuccess"));
antdMessage.success($t("preferences.importPreferencesSuccess"));
} catch {
antdMessage.error($t("preferences.importPreferencesError"));
}
}
async function handleSaveToAccount() {
if (!userStore.isLogined) {
antdMessage.warning($t("preferences.saveToAccountNeedLogin"));
return;
}
if (savingToAccount.value) {
return;
}
savingToAccount.value = true;
try {
// 重置后 diff 为空时保存 {};后续登录拉取到空数据则继续使用本地偏好
await savePreferencesToAccount((diffPreference.value as Record<string, any>) || {});
} catch (e: any) {
antdMessage.error(e?.message || $t("preferences.saveToAccountError"));
} finally {
savingToAccount.value = false;
}
} }
async function handleClearCache() { async function handleClearCache() {
@@ -351,25 +310,13 @@ async function handleReset() {
</div> </div>
<template #footer> <template #footer>
<div class="flex w-full flex-col gap-2 px-1"> <VbenButton :disabled="!diffPreference" class="mx-4 w-full" size="sm" variant="default" @click="handleCopy">
<div class="flex w-full gap-2">
<VbenButton :disabled="!diffPreference" class="w-full" size="sm" variant="default" @click="handleCopy">
<Copy class="mr-2 size-3" /> <Copy class="mr-2 size-3" />
{{ $t("preferences.copyPreferences") }} {{ $t("preferences.copyPreferences") }}
</VbenButton> </VbenButton>
<VbenButton class="w-full" size="sm" variant="outline" @click="handleImport"> <VbenButton :disabled="!diffPreference" class="mr-4 w-full" size="sm" variant="ghost" @click="handleClearCache">
<ClipboardPaste class="mr-2 size-3" />
{{ $t("preferences.importPreferences") }}
</VbenButton>
</div>
<VbenButton :disabled="!userStore.isLogined || savingToAccount" class="w-full" size="sm" variant="outline" @click="handleSaveToAccount">
<CloudUpload class="mr-2 size-3" />
{{ $t("preferences.saveToAccount") }}
</VbenButton>
<VbenButton :disabled="!diffPreference" class="w-full" size="sm" variant="ghost" @click="handleClearCache">
{{ $t("preferences.clearAndLogout") }} {{ $t("preferences.clearAndLogout") }}
</VbenButton> </VbenButton>
</div>
</template> </template>
</Drawer> </Drawer>
</div> </div>
@@ -17,7 +17,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, ref, watch, inject, onMounted, Ref } from "vue"; import { defineComponent, reactive, ref, watch, inject, onMounted } from "vue";
import CertAccessModal from "./access/index.vue"; import CertAccessModal from "./access/index.vue";
import { createAccessApi } from "../api"; import { createAccessApi } from "../api";
import { message } from "ant-design-vue"; import { message } from "ant-design-vue";
@@ -64,9 +64,9 @@ export default defineComponent({
setup(props, ctx) { setup(props, ctx) {
const api = createAccessApi(props.from); const api = createAccessApi(props.from);
const target:Ref<any> = ref({}); const target = ref({});
const selectedId = ref(); const selectedId = ref();
async function refreshTarget(value:any) { async function refreshTarget(value) {
selectedId.value = value; selectedId.value = value;
if (value > 0) { if (value > 0) {
target.value = await api.GetSimpleInfo(value); target.value = await api.GetSimpleInfo(value);
@@ -83,7 +83,7 @@ export default defineComponent({
const userStore = useUserStore(); const userStore = useUserStore();
const projectStore = useProjectStore(); const projectStore = useProjectStore();
async function emitValue(value:any) { async function emitValue(value) {
const userId = userStore.userInfo.id; const userId = userStore.userInfo.id;
const isEnterprice = projectStore.isEnterprise; const isEnterprice = projectStore.isEnterprise;
if (pipeline?.value) { if (pipeline?.value) {
@@ -132,7 +132,7 @@ export default defineComponent({
const providerDefine = ref({}); const providerDefine = ref({});
async function refreshProviderDefine(type:any) { async function refreshProviderDefine(type) {
providerDefine.value = await api.GetProviderDefine(type); providerDefine.value = await api.GetProviderDefine(type);
} }
watch( watch(
@@ -1,40 +0,0 @@
/// <reference types="mocha" />
/// <reference types="node" />
import assert from "node:assert/strict";
import { parseUserPreferencesPayload } from "./user-preferences.js";
describe("parseUserPreferencesPayload", () => {
it("parses wrapped preferences payload", () => {
const result = parseUserPreferencesPayload({
preferences: {
theme: { mode: "dark" },
},
});
assert.deepEqual(result, { theme: { mode: "dark" } });
});
it("parses direct preferences payload", () => {
const result = parseUserPreferencesPayload({
app: { locale: "en-US" },
theme: { mode: "light" },
});
assert.deepEqual(result, {
app: { locale: "en-US" },
theme: { mode: "light" },
});
});
it("parses empty preferences as reset payload", () => {
assert.deepEqual(parseUserPreferencesPayload({}), {});
assert.deepEqual(parseUserPreferencesPayload({ preferences: {} }), {});
});
it("returns null for invalid payload", () => {
assert.equal(parseUserPreferencesPayload(null), null);
assert.equal(parseUserPreferencesPayload([]), null);
assert.equal(parseUserPreferencesPayload({ foo: 1 }), null);
assert.equal(parseUserPreferencesPayload({ preferences: { foo: 1 } }), null);
});
});
@@ -1,41 +0,0 @@
const PREFERENCES_KNOWN_KEYS = [
"app",
"theme",
"logo",
"sidebar",
"header",
"tabbar",
"breadcrumb",
"navigation",
"widget",
"footer",
"copyright",
"shortcutKeys",
"transition",
];
/**
* 解析用户偏好设置 payload。
* 兼容 `{ preferences: {...} }` 与直接传偏好对象两种格式。
* 空对象 `{}` 表示已重置为默认偏好,视为合法。
*/
export function parseUserPreferencesPayload(value: unknown): Record<string, any> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const raw = value as Record<string, any>;
const preferences =
raw.preferences != null && typeof raw.preferences === "object" && !Array.isArray(raw.preferences)
? (raw.preferences as Record<string, any>)
: raw;
const preferenceKeys = Object.keys(preferences);
// 空对象表示重置后的默认偏好
if (preferenceKeys.length === 0) {
return {};
}
const hasKnownKey = PREFERENCES_KNOWN_KEYS.some(key => Object.prototype.hasOwnProperty.call(preferences, key));
if (!hasKnownKey) {
return null;
}
return preferences;
}
@@ -2,11 +2,10 @@ import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/c
import { Constants, CrudController } from "@certd/lib-server"; import { Constants, CrudController } from "@certd/lib-server";
import { UserSettingsService } from "../../../modules/mine/service/user-settings-service.js"; import { UserSettingsService } from "../../../modules/mine/service/user-settings-service.js";
import { UserSettingsEntity } from "../../../modules/mine/entity/user-settings.js"; import { UserSettingsEntity } from "../../../modules/mine/entity/user-settings.js";
import { UserGrantSetting, UserPreferencesSetting } from "../../../modules/mine/service/models.js"; import { UserGrantSetting } from "../../../modules/mine/service/models.js";
import { isPlus } from "@certd/plus-core"; import { isPlus } from "@certd/plus-core";
import { merge } from "lodash-es"; import { merge } from "lodash-es";
import { ApiTags } from "@midwayjs/swagger"; import { ApiTags } from "@midwayjs/swagger";
import { parseUserPreferencesPayload } from "./user-preferences.js";
/** /**
*/ */
@@ -91,37 +90,4 @@ export class UserSettingsController extends CrudController<UserSettingsService>
await this.service.saveSetting(userId, null, setting); await this.service.saveSetting(userId, null, setting);
return this.ok({}); return this.ok({});
} }
@Post("/preferences/get", { description: Constants.per.authOnly, summary: "获取用户偏好设置" })
async preferencesGet() {
const userId = this.getUserId();
const entity = await this.service.getByKey(UserPreferencesSetting.__key__, userId, null);
if (!entity?.setting) {
return this.ok(null);
}
let parsed: unknown;
try {
parsed = JSON.parse(entity.setting);
} catch {
return this.ok(null);
}
return this.ok(parseUserPreferencesPayload(parsed));
}
@Post("/preferences/save", { description: Constants.per.authOnly, summary: "保存用户偏好设置" })
async preferencesSave(@Body(ALL) bean: any) {
const userId = this.getUserId();
const preferences = parseUserPreferencesPayload(bean);
if (!preferences) {
throw new Error("偏好设置内容无效");
}
// 整份替换,避免 saveSetting 深合并留下已恢复为默认值的旧字段
const entity = new UserSettingsEntity();
entity.key = UserPreferencesSetting.__key__;
entity.title = UserPreferencesSetting.__title__;
entity.userId = userId;
entity.setting = JSON.stringify({ preferences });
await this.service.save(entity);
return this.ok({});
}
} }
@@ -52,14 +52,6 @@ export class UserGrantSetting extends BaseSettings {
allowAdminViewCerts = false; allowAdminViewCerts = false;
} }
export class UserPreferencesSetting extends BaseSettings {
static __title__ = "用户偏好设置";
static __key__ = "user.preferences";
/** 偏好差异配置(相对默认值),与前端 diffPreference 结构一致 */
preferences: Record<string, any> = {};
}
export class UserDomainImportSetting extends BaseSettings { export class UserDomainImportSetting extends BaseSettings {
static __title__ = "用户域名导入设置"; static __title__ = "用户域名导入设置";
static __key__ = "user.domain.import"; static __key__ = "user.domain.import";
@@ -5,7 +5,6 @@ import { IContext } from "@certd/pipeline";
import { IDnsProvider, IDomainParser } from "@certd/plugin-lib"; import { IDnsProvider, IDomainParser } from "@certd/plugin-lib";
import punycode from "punycode.js"; import punycode from "punycode.js";
import { IOssClient } from "../../../plugin-lib/index.js"; import { IOssClient } from "../../../plugin-lib/index.js";
import { NonRetryableException } from "@certd/lib-server";
export type CnameVerifyPlan = { export type CnameVerifyPlan = {
type?: string; type?: string;
domain: string; domain: string;
@@ -532,7 +531,6 @@ export class AcmeService {
}; };
/* 自动申请证书 */ /* 自动申请证书 */
const challengePriority = domainsVerifyPlan && Object.values(domainsVerifyPlan).some((item: any) => item?.type === "dns-persist") ? ["dns-persist-01"] : ["dns-01", "http-01"]; const challengePriority = domainsVerifyPlan && Object.values(domainsVerifyPlan).some((item: any) => item?.type === "dns-persist") ? ["dns-persist-01"] : ["dns-01", "http-01"];
try {
const crt = await client.auto({ const crt = await client.auto({
csr, csr,
email: email, email: email,
@@ -565,14 +563,6 @@ export class AcmeService {
this.logger.debug(`Certificate:\n${cert.crt}`); this.logger.debug(`Certificate:\n${cert.crt}`);
this.logger.info("证书申请成功"); this.logger.info("证书申请成功");
return cert; return cert;
} catch (e) {
const message = e?.message;
const REDUNDANT_WILDCARD_DOMAIN_ERROR = "redundant with a wildcard domain in the same request";
if (message != null && message.indexOf(REDUNDANT_WILDCARD_DOMAIN_ERROR) >= 0) {
throw new NonRetryableException(`通配符域名已经包含了普通域名,请删除其中一个(${message}`);
}
throw e;
}
} }
buildCommonNameByDomains(domains: string | string[]): { buildCommonNameByDomains(domains: string | string[]): {
@@ -1,6 +1,4 @@
import assert from "assert"; import assert from "assert";
import { utils } from "@certd/basic";
import { NonRetryableException } from "@certd/lib-server";
import { CertApplyPlugin } from "./apply.js"; import { CertApplyPlugin } from "./apply.js";
describe("CertApplyPlugin dns-persist verify plan", () => { describe("CertApplyPlugin dns-persist verify plan", () => {
@@ -47,114 +45,3 @@ describe("CertApplyPlugin dns-persist verify plan", () => {
assert.equal(plan["handfree.work"].dnsPersistVerifyPlan?.recordValue, "letsencrypt.org; accounturi=https://acme.example/acct/1; policy=wildcard"); assert.equal(plan["handfree.work"].dnsPersistVerifyPlan?.recordValue, "letsencrypt.org; accounturi=https://acme.example/acct/1; policy=wildcard");
}); });
}); });
describe("CertApplyPlugin certificate apply retry", () => {
it("does not retry by default", async () => {
const plugin: any = new CertApplyPlugin();
let orderCount = 0;
const error = new Error("apply failed");
plugin.logger = { warn() {} };
plugin.acme = {
async order() {
orderCount++;
throw error;
},
};
await assert.rejects(plugin.orderWithRetry({}), error);
assert.equal(orderCount, 1);
assert.equal(plugin.certApplyRetryCount, 0);
});
it("retries after a 30-second cooldown and succeeds before reaching the limit", async () => {
const plugin: any = new CertApplyPlugin();
let orderCount = 0;
const waitTimes: number[] = [];
plugin.certApplyRetryCount = 2;
plugin.logger = { warn() {} };
plugin.acme = {
async order() {
orderCount++;
if (orderCount < 3) {
throw new Error(`apply failed ${orderCount}`);
}
return { crt: "certificate", key: "private-key" };
},
};
const originalSleep = utils.sleep;
utils.sleep = async (waitTime: number) => {
waitTimes.push(waitTime);
};
try {
const cert = await plugin.orderWithRetry({});
assert.deepEqual(cert, { crt: "certificate", key: "private-key" });
assert.equal(orderCount, 3);
assert.deepEqual(waitTimes, [30_000, 30_000]);
} finally {
utils.sleep = originalSleep;
}
});
it("throws the last error after reaching the retry limit", async () => {
const plugin: any = new CertApplyPlugin();
let orderCount = 0;
const error = new Error("apply failed");
plugin.certApplyRetryCount = 1;
plugin.logger = { warn() {} };
plugin.acme = {
async order() {
orderCount++;
throw error;
},
};
const originalSleep = utils.sleep;
utils.sleep = async () => {};
try {
await assert.rejects(plugin.orderWithRetry({}), error);
assert.equal(orderCount, 2);
} finally {
utils.sleep = originalSleep;
}
});
it("does not retry a cancelled apply", async () => {
const plugin: any = new CertApplyPlugin();
let orderCount = 0;
const error: any = new Error("cancelled");
error.name = "CancelError";
plugin.certApplyRetryCount = 2;
plugin.logger = { warn() {} };
plugin.acme = {
async order() {
orderCount++;
throw error;
},
};
await assert.rejects(plugin.orderWithRetry({}), error);
assert.equal(orderCount, 1);
});
it("throws a non-retryable error for wildcard and normal domain conflicts", async () => {
const plugin: any = new CertApplyPlugin();
let orderCount = 0;
const message = "example.com is redundant with a wildcard domain in the same request";
plugin.certApplyRetryCount = 2;
plugin.logger = { warn() {} };
plugin.acme = {
async order() {
orderCount++;
throw new NonRetryableException(`通配符域名已经包含了普通域名,请删除其中一个(${message}`);
},
};
await assert.rejects(plugin.orderWithRetry({}), (error: any) => {
assert.equal(error instanceof NonRetryableException, true);
assert.equal(error.message, `通配符域名已经包含了普通域名,请删除其中一个(${message}`);
return true;
});
assert.equal(orderCount, 1);
});
});
@@ -1,6 +1,5 @@
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; import { CancelError, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { utils } from "@certd/basic"; import { utils } from "@certd/basic";
import { NonRetryableException } from "@certd/lib-server";
import { AcmeAccountInfo, AcmeService, DomainsVerifyPlan, DomainVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js"; import { AcmeAccountInfo, AcmeService, DomainsVerifyPlan, DomainVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
import { createDnsProvider, DnsProviderContext, DnsVerifier, DomainVerifiers, HttpVerifier, IDnsProvider, IDomainVerifierGetter, ISubDomainsGetter } from "@certd/plugin-lib"; import { createDnsProvider, DnsProviderContext, DnsVerifier, DomainVerifiers, HttpVerifier, IDnsProvider, IDomainVerifierGetter, ISubDomainsGetter } from "@certd/plugin-lib";
@@ -62,7 +61,6 @@ const preferredChainConfigs = {
} as const; } as const;
const preferredChainSupportedProviders = Object.keys(preferredChainConfigs); const preferredChainSupportedProviders = Object.keys(preferredChainConfigs);
const CERT_APPLY_RETRY_DELAY_MS = 30_000;
const preferredChainMergeScript = (() => { const preferredChainMergeScript = (() => {
const configs = JSON.stringify(preferredChainConfigs); const configs = JSON.stringify(preferredChainConfigs);
@@ -553,20 +551,6 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
}) })
waitDnsDiffuseTime = 30; waitDnsDiffuseTime = 30;
@TaskInput({
title: "证书申请失败重试次数",
value: 0,
component: {
name: "a-input-number",
vModel: "value",
min: 0,
step: 1,
},
maybeNeed: true,
helper: "证书申请失败后,等待30秒再自动重试;0表示不重试",
})
certApplyRetryCount = 0;
acme!: AcmeService; acme!: AcmeService;
eab!: EabAccess; eab!: EabAccess;
@@ -667,7 +651,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
dnsProvider = await this.createDnsProvider(dnsProviderType, access); dnsProvider = await this.createDnsProvider(dnsProviderType, access);
} }
const cert = await this.orderWithRetry({ try {
const cert = await this.acme.order({
email, email,
domains, domains,
dnsProvider, dnsProvider,
@@ -681,39 +666,19 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
const certInfo = this.formatCerts(cert); const certInfo = this.formatCerts(cert);
return new CertReader(certInfo); return new CertReader(certInfo);
}
private async orderWithRetry(orderOptions: Parameters<AcmeService["order"]>[0]) {
const maxRetryCount = this.getCertApplyRetryCount();
let retryCount = 0;
while (true) {
try {
return await this.acme.order(orderOptions);
} catch (e: any) { } catch (e: any) {
if (e instanceof NonRetryableException) { const message: string = e?.message;
if (message != null && message.indexOf("redundant with a wildcard domain in the same request") >= 0) {
this.logger.error(e);
throw new Error(`通配符域名已经包含了普通域名,请删除其中一个(${message}`);
}
if (e.name === "CancelError") {
throw new CancelError(e.message);
}
throw e; throw e;
} }
if (e?.name === "CancelError" || retryCount >= maxRetryCount) {
throw e;
} }
retryCount++;
this.logger.warn(`证书申请失败,等待30秒后重试(${retryCount}/${maxRetryCount}`, e);
await utils.sleep(CERT_APPLY_RETRY_DELAY_MS);
}
}
}
private getCertApplyRetryCount() {
const retryCount = Number(this.certApplyRetryCount);
if (!Number.isFinite(retryCount) || retryCount <= 0) {
return 0;
}
return Math.floor(retryCount);
}
async createDnsProvider(dnsProviderType: string, dnsProviderAccess: any): Promise<IDnsProvider> { async createDnsProvider(dnsProviderType: string, dnsProviderAccess: any): Promise<IDnsProvider> {
const domainParser = this.acme.options.domainParser; const domainParser = this.acme.options.domainParser;
const context: DnsProviderContext = { const context: DnsProviderContext = {
@@ -76,7 +76,7 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
this.clearLastStatus(); this.clearLastStatus();
if (this.successNotify) { if (this.successNotify) {
await this.sendSuccessNotify(cert); await this.sendSuccessNotify();
} }
} else { } else {
throw new Error("申请证书失败"); throw new Error("申请证书失败");
@@ -165,14 +165,12 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
nextUpdateDays: leftDays - maxDays, nextUpdateDays: leftDays - maxDays,
}; };
} }
async sendSuccessNotify(certReader: CertReader) { async sendSuccessNotify() {
this.logger.info("发送证书申请成功通知"); this.logger.info("发送证书申请成功通知");
const url = await this.ctx.urlService.getPipelineDetailUrl(this.pipeline.id, this.ctx.runtime.id); const url = await this.ctx.urlService.getPipelineDetailUrl(this.pipeline.id, this.ctx.runtime.id);
const body: NotificationBody = { const body: NotificationBody = {
title: `证书申请成功【${this.pipeline.title}`, title: `证书申请成功【${this.pipeline.title}`,
content: `域名:${this.domains.join(",")}\n content: `域名:${this.domains.join(",")}`,
${dayjs(certReader.expires).format("YYYY-MM-DD HH:mm:ss")}\n
`,
url: url, url: url,
notificationType: "certApplySuccess", notificationType: "certApplySuccess",
}; };
@@ -1,17 +1,15 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import { BaotaAccess } from "../access.js"; import { BaotaAccess } from "../access.js";
import { HttpClient, HttpRequestConfig, ILogger } from "@certd/basic"; import { HttpClient, HttpRequestConfig } from "@certd/basic";
import * as querystring from "node:querystring"; import * as querystring from "node:querystring";
export class BaotaClient { export class BaotaClient {
access: BaotaAccess; access: BaotaAccess;
http: HttpClient; http: HttpClient;
logger: ILogger
constructor(access: BaotaAccess, http: HttpClient) { constructor(access: BaotaAccess, http: HttpClient) {
this.access = access; this.access = access;
this.http = http; this.http = http;
this.logger = access.ctx.logger;
} }
//将以上 java代码 翻译成nodejs 代码 //将以上 java代码 翻译成nodejs 代码
@@ -88,7 +88,7 @@ export class BaotaDeployWebSiteCert extends AbstractTaskPlugin {
}) })
siteName!: string | string[]; siteName!: string | string[];
async onInstance() { } async onInstance() {}
async execute(): Promise<void> { async execute(): Promise<void> {
const { cert, accessId } = this; const { cert, accessId } = this;
const access = await this.getAccess(accessId); const access = await this.getAccess(accessId);
@@ -104,15 +104,12 @@ export class BaotaDeployWebSiteCert extends AbstractTaskPlugin {
} }
const lockKey = `baota-lock-${accessId}`; const lockKey = `baota-lock-${accessId}`;
if (this.isDockerSite) {
this.logger.info(`当前已勾选docker站点(如果部署失败,请确认站点:${siteNames} 是否全部为docker站点)`);
}
for (const site of siteNames) { for (const site of siteNames) {
// 加锁,防止并发部署证书, 宝塔并发部署会导致nginx的conf错乱 // 加锁,防止并发部署证书, 宝塔并发部署会导致nginx的conf错乱
await this.ctx.utils.locker.execute(lockKey, async () => { await this.ctx.utils.locker.execute(lockKey, async () => {
try { this.logger.info(`为站点:${site}设置证书,目前支持宝塔网站站点、docker站点`);
if (this.isDockerSite) { if (this.isDockerSite) {
this.logger.info(`为Docker站点:${site} 设置证书`);
const res = await client.doRequest("/mod/docker/com/set_ssl", "", { const res = await client.doRequest("/mod/docker/com/set_ssl", "", {
site_name: site, site_name: site,
key: cert.key, key: cert.key,
@@ -120,7 +117,6 @@ export class BaotaDeployWebSiteCert extends AbstractTaskPlugin {
}); });
this.logger.info(res?.msg); this.logger.info(res?.msg);
} else { } else {
this.logger.info(`为非Docker站点:${site} 设置证书`);
const res = await client.doRequest("/site", "SetSSL", { const res = await client.doRequest("/site", "SetSSL", {
type: 0, type: 0,
siteName: site, siteName: site,
@@ -129,13 +125,6 @@ export class BaotaDeployWebSiteCert extends AbstractTaskPlugin {
}); });
this.logger.info(res?.msg); this.logger.info(res?.msg);
} }
} catch (e: any) {
if (e?.message?.includes("没有服务器配置文件")) {
this.logger.error(e.message);
this.logger.warn(`首先请确认站点 ${site} 是否存在,如果存在,请到宝塔上手动保存一下该站点证书试试`);
}
throw e
}
}); });
} }
@@ -9,7 +9,7 @@ import { TencentAccess, TencentSslClient } from "../../../plugin-lib/tencent/ind
title: "腾讯云-删除即将过期证书", title: "腾讯云-删除即将过期证书",
icon: "svg:icon-tencentcloud", icon: "svg:icon-tencentcloud",
group: pluginGroups.tencent.key, group: pluginGroups.tencent.key,
desc: "仅删除即将过期且未使用的证书", desc: "仅删除未使用的证书",
dependPlugins: { dependPlugins: {
"access:tencent": "*", "access:tencent": "*",
}, },
@@ -1,111 +0,0 @@
/// <reference types="mocha" />
import assert from "node:assert/strict";
import { DeployCertToTencentCLB } from "./index.js";
describe("DeployCertToTencentCLB", () => {
it("uses remote single-select inputs for CLB and HTTPS listener", () => {
const input = (DeployCertToTencentCLB as any).define.input;
assert.equal(input.loadBalancerId.component.name, "remote-select");
assert.equal(input.loadBalancerId.component.single, true);
assert.equal(input.loadBalancerId.component.action, "onGetCLBList");
assert.equal(input.loadBalancerId.required, true);
assert.equal(input.listenerId.component.name, "remote-select");
assert.equal(input.listenerId.component.single, true);
assert.equal(input.listenerId.component.action, "onGetListenerList");
assert.deepEqual(input.listenerId.component.watches, ["certDomains", "accessId", "region", "loadBalancerId"]);
assert.equal(input.listenerId.required, true);
assert.equal(input.domain.component.name, "remote-select");
assert.equal(input.domain.component.single, false);
assert.equal(input.domain.component.action, "onGetDomainList");
assert.deepEqual(input.domain.component.watches, ["certDomains", "accessId", "region", "loadBalancerId", "listenerId"]);
assert.equal(input.domain.required, false);
});
it("maps CLB API results to remote-select options", async () => {
const plugin = new DeployCertToTencentCLB();
plugin.accessId = "access-1";
plugin.logger = { info: () => undefined } as any;
(plugin as any).getClient = async () => ({});
(plugin as any).getCLBList = async () => [
{
LoadBalancerId: "lb-1",
LoadBalancerName: "业务负载均衡",
},
];
const options = await plugin.onGetCLBList({});
assert.deepEqual(options, [
{
value: "lb-1",
label: "业务负载均衡<lb-1>",
},
]);
});
it("maps HTTPS listener API results after selecting a CLB", async () => {
const plugin = new DeployCertToTencentCLB();
plugin.accessId = "access-1";
plugin.loadBalancerId = "lb-1";
plugin.logger = { info: () => undefined } as any;
(plugin as any).getClient = async () => ({});
(plugin as any).getListenerList = async (_client: any, loadBalancerId: string, listenerIds: any) => {
assert.equal(loadBalancerId, "lb-1");
assert.equal(listenerIds, null);
return [
{
ListenerId: "listener-1",
ListenerName: "HTTPS监听器",
Port: 443,
},
];
};
const options = await plugin.onGetListenerList({});
assert.deepEqual(options, [
{
value: "listener-1",
label: "HTTPS监听器:443<listener-1>",
},
]);
});
it("maps SNI domains from the selected listener rules", async () => {
const plugin = new DeployCertToTencentCLB();
plugin.accessId = "access-1";
plugin.loadBalancerId = "lb-1";
plugin.listenerId = "listener-1";
plugin.logger = { info: () => undefined } as any;
(plugin as any).getClient = async () => ({});
(plugin as any).getListenerList = async (_client: any, loadBalancerId: string, listenerIds: string[]) => {
assert.equal(loadBalancerId, "lb-1");
assert.deepEqual(listenerIds, ["listener-1"]);
return [
{
ListenerId: "listener-1",
Rules: [{ Domain: "www.example.com" }, { Domain: "api.example.com" }],
},
];
};
const options = await plugin.onGetDomainList({});
assert.deepEqual(options, [
{
value: "www.example.com",
label: "www.example.com",
domain: "www.example.com",
},
{
value: "api.example.com",
label: "api.example.com",
domain: "api.example.com",
},
]);
});
});
@@ -1,8 +1,7 @@
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { TencentAccess } from "../../../plugin-lib/tencent/index.js"; import { TencentAccess } from "../../../plugin-lib/tencent/index.js";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert"; import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { createRemoteSelectInputDefine } from "@certd/plugin-lib";
@IsTaskPlugin({ @IsTaskPlugin({
name: "DeployCertToTencentCLB", name: "DeployCertToTencentCLB",
title: "腾讯云-部署到CLB", title: "腾讯云-部署到CLB",
@@ -74,44 +73,29 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
}) })
region!: string; region!: string;
@TaskInput( @TaskInput({
createRemoteSelectInputDefine({
title: "负载均衡ID", title: "负载均衡ID",
helper: "请选择要部署证书的负载均衡", required: true,
action: DeployCertToTencentCLB.prototype.onGetCLBList.name,
watches: ["region"],
single: true,
pager: false,
search: false,
}) })
)
loadBalancerId!: string; loadBalancerId!: string;
@TaskInput( @TaskInput({
createRemoteSelectInputDefine({
title: "监听器ID", title: "监听器ID",
helper: "请选择要部署证书的HTTPS监听器", required: true,
action: DeployCertToTencentCLB.prototype.onGetListenerList.name,
watches: ["region", "loadBalancerId"],
single: true,
pager: false,
search: false,
}) })
)
listenerId!: string; listenerId!: string;
@TaskInput( @TaskInput({
createRemoteSelectInputDefine({
title: "域名", title: "域名",
helper: "如果开启了SNI,请选择要部署证书的域名;未开启SNI时可以留空",
action: DeployCertToTencentCLB.prototype.onGetDomainList.name,
watches: ["region", "loadBalancerId", "listenerId"],
required: false, required: false,
single: false, component: {
pager: false, name: "a-select",
search: false, vModel: "value",
open: false,
mode: "tags",
},
helper: "如果开启了sni,则此项必须填写,未开启,则不要填写",
}) })
)
domain!: string | string[]; domain!: string | string[];
@TaskInput({ @TaskInput({
@@ -288,27 +272,6 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
return ret.LoadBalancerSet; return ret.LoadBalancerSet;
} }
async onGetCLBList(data: PageSearch) {
if (!this.accessId) {
throw new Error("请选择Access提供者");
}
const client = await this.getClient();
const list = await this.getCLBList(client);
if (!list || list.length === 0) {
return [];
}
return list.map((item: any) => {
const loadBalancerId = item.LoadBalancerId;
const loadBalancerName = item.LoadBalancerName || loadBalancerId;
return {
value: loadBalancerId,
label: `${loadBalancerName}<${loadBalancerId}>`,
};
});
}
async getListenerList(client: any, balancerId: any, listenerIds: any) { async getListenerList(client: any, balancerId: any, listenerIds: any) {
// HTTPS // HTTPS
const params = { const params = {
@@ -321,57 +284,6 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
return ret.Listeners; return ret.Listeners;
} }
async onGetListenerList(data: PageSearch) {
if (!this.accessId) {
throw new Error("请选择Access提供者");
}
if (!this.loadBalancerId) {
throw new Error("请先选择负载均衡");
}
const client = await this.getClient();
const list = await this.getListenerList(client, this.loadBalancerId, null);
if (!list || list.length === 0) {
return [];
}
return list.map((item: any) => {
const listenerId = item.ListenerId;
const listenerName = item.ListenerName || "HTTPS监听器";
const port = item.Port ? `:${item.Port}` : "";
return {
value: listenerId,
label: `${listenerName}${port}<${listenerId}>`,
};
});
}
async onGetDomainList(data: PageSearch) {
if (!this.accessId) {
throw new Error("请选择Access提供者");
}
if (!this.loadBalancerId) {
throw new Error("请先选择负载均衡");
}
if (!this.listenerId) {
throw new Error("请先选择监听器");
}
const client = await this.getClient();
const listeners = await this.getListenerList(client, this.loadBalancerId, [this.listenerId]);
const listener = listeners?.[0];
const domains = listener?.Rules?.map((rule: any) => rule.Domain).filter(Boolean) || [];
const uniqueDomains = [...new Set(domains)];
return uniqueDomains.map(domain => {
return {
value: domain,
label: domain,
domain,
};
});
}
checkRet(ret: any) { checkRet(ret: any) {
if (!ret || ret.Error) { if (!ret || ret.Error) {
throw new Error("执行失败:" + ret.Error.Code + "," + ret.Error.Message); throw new Error("执行失败:" + ret.Error.Code + "," + ret.Error.Message);